use std::time::Duration;

/// Represents a temporary denial of the credential to authenticate. This is used
/// to ratelimit and prevent bruteforcing of accounts. At an initial failure the
/// SoftLock is created and the count set to 1, with a unlock_at set to 1 second
/// later, and a reset_count_at: at a maximum time window for a cycle.
///
/// If the softlock already exists, and the failure count is 0, then this acts as the
/// creation where the reset_count_at window is then set.
///
/// While current_time < unlock_at, all authentication attempts are denied with a
/// message regarding the account being temporarily unavailable. Once
/// unlock_at < current_time, authentication will be processed again. If a subsequent
/// failure occurs, unlock_at is extended based on policy, and failure_count incremented.
///
/// If unlock_at < current_time, and authentication succeeds the login is allowed
/// and no changes to failure_count or unlock_at are made.
///
/// If reset_count_at < current_time, then failure_count is reset to 0 before processing.
///
/// This allows handling of max_failure_count, so that when that value from policy is
/// exceeded then unlock_at is set to reset_count_at to softlock until the cycle
/// is over (see NIST sp800-63b.). For example, reset_count_at will be 24 hours after
/// the first failed authentication attempt.
///
/// This also works for something like TOTP which allows a 60 second cycle for the
/// reset_count_at and a max number of attempts in that window (say 5). with short
/// delays in between (1 second).
//
//                                                  ┌────────────────────────┐
//                                                  │reset_at < current_time │
//                                                 ─└────────────────────────┘
//                                                │                         │
//                                                ▼
//             ┌─────┐                         .─────.       ┌────┐         │
//             │Valid│                        ╱       ╲      │Fail│
//        ┌────┴─────┴───────────────────────(count = 0)─────┴────┴┐        │
//        │                                   `.     ,'            │
//        │                                     `───'              │        │
//        │             ┌────────────────────────┐▲                │
//        │             │reset_at < current_time │                 │        │
//        │             └────────────────────────┘│                │
//        │                      ┌ ─ ─ ─ ─ ─ ─ ─ ─                 │        │
//        │                                                        │
//        │                      ├─────┬───────┬──┐                ▼        │
//        │                      │     │ Fail  │  │             .─────.
//        │                      │     │count++│  │           ,'       `.   │
//        ▼                   .─────.  └───────┘  │          ;  Locked   :
// ┌────────────┐            ╱       ╲            └─────────▶: count > 0 ;◀─┤
// │Auth Success│◀─┬─────┬──(Unlocked )                       ╲         ╱   │
// └────────────┘  │Valid│   `.     ,'                         `.     ,'    │
//                 └─────┘     `───'                             `───'      │
//                               ▲                                 │        │
//                               │                                 │        │
//                               └─────┬──────────────────────────┬┴┬───────┴──────────────────┐
//                                     │ expire_at < current_time │ │ current_time < expire_at │
//                                     └──────────────────────────┘ └──────────────────────────┘
//
//

const ONEDAY: u64 = 86400;

#[derive(Debug, Clone)]
pub enum CredSoftLockPolicy {
    Password,
    Totp(u64),
    Webauthn,
    Unrestricted,
}

impl CredSoftLockPolicy {
    /// Determine the next lock state after a failure based on this credentials
    /// policy.
    fn failure_next_state(&self, count: usize, ct: Duration) -> LockState {
        match self {
            CredSoftLockPolicy::Password => {
                let next_day_end = ct.as_secs() + ONEDAY;
                let rem = next_day_end % ONEDAY;
                let reset_at = Duration::from_secs(next_day_end - rem);

                if count < 3 {
                    LockState::Locked(count, reset_at, ct + Duration::from_secs(1))
                } else if count < 9 {
                    LockState::Locked(count, reset_at, ct + Duration::from_secs(3))
                } else if count < 25 {
                    LockState::Locked(count, reset_at, ct + Duration::from_secs(5))
                } else if count < 100 {
                    LockState::Locked(count, reset_at, ct + Duration::from_secs(10))
                } else {
                    LockState::Locked(count, reset_at, reset_at)
                }
            }
            CredSoftLockPolicy::Totp(step) => {
                // reset at is based on the next step ending.
                let next_window_end = ct.as_secs() + step;
                let rem = next_window_end % step;
                let reset_at = Duration::from_secs(next_window_end - rem);
                // We delay for 1 second, unless count is > 3, then we set
                // unlock at to reset_at.
                if count >= 3 {
                    LockState::Locked(count, reset_at, reset_at)
                } else {
                    LockState::Locked(count, reset_at, ct + Duration::from_secs(1))
                }
            }
            CredSoftLockPolicy::Webauthn => {
                // we only lock for 1 second to slow them down.
                // TODO: Could this be a DOS/Abuse vector?
                LockState::Locked(
                    count,
                    ct + Duration::from_secs(1),
                    ct + Duration::from_secs(1),
                )
            }
            CredSoftLockPolicy::Unrestricted => {
                // No action needed
                LockState::Init
            }
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum LockState {
    Init,
    // count
    // * Number of Failures in this cycle
    // unlock_at
    // * Time of next allowed check (works with delay)
    // reset_count_at
    // * The time to reset the state to init.
    //     count  reset_at  unlock_at
    Locked(usize, Duration, Duration),
    Unlocked(usize, Duration),
}

#[derive(Debug, Clone)]
pub(crate) struct CredSoftLock {
    state: LockState,
    // Policy (for determining delay times based on num failures, and when to reset?)
    policy: CredSoftLockPolicy,
}

impl CredSoftLock {
    pub fn new(policy: CredSoftLockPolicy) -> Self {
        CredSoftLock {
            state: LockState::Init,
            policy,
        }
    }

    pub fn apply_time_step(&mut self, ct: Duration) {
        // Do a reset if needed?
        let mut next_state = match self.state {
            LockState::Init => LockState::Init,
            LockState::Locked(count, reset_at, unlock_at) => {
                if ct > reset_at {
                    LockState::Init
                } else if ct > unlock_at {
                    LockState::Unlocked(count, reset_at)
                } else {
                    LockState::Locked(count, reset_at, unlock_at)
                }
            }
            LockState::Unlocked(count, reset_at) => {
                if ct > reset_at {
                    LockState::Init
                } else {
                    LockState::Unlocked(count, reset_at)
                }
            }
        };
        std::mem::swap(&mut self.state, &mut next_state);
    }

    /// Is this credential valid to proceed at this point in time.
    pub fn is_valid(&self) -> bool {
        !matches!(self.state, LockState::Locked(_count, _reset_at, _unlock_at))
    }

    /// Document a failure of authentication at this time.
    pub fn record_failure(&mut self, ct: Duration) {
        let mut next_state = match self.state {
            LockState::Init => {
                self.policy.failure_next_state(1, ct)
                // LockState::Locked(1, reset_at, unlock_at)
            }
            LockState::Locked(count, _reset_at, _unlock_at) => {
                // We should never reach this but just in case ...
                self.policy.failure_next_state(count + 1, ct)
                // LockState::Locked(count + 1, reset_at, unlock_at)
            }
            LockState::Unlocked(count, _reset_at) => {
                self.policy.failure_next_state(count + 1, ct)
                // LockState::Locked(count + 1, reset_at, unlock_at)
            }
        };
        std::mem::swap(&mut self.state, &mut next_state);
    }

    #[cfg(test)]
    pub fn is_state_init(&self) -> bool {
        matches!(self.state, LockState::Init)
    }

    #[cfg(test)]
    fn peek_state(&self) -> &LockState {
        &self.state
    }

    /*
    #[cfg(test)]
    fn set_failure_count(&mut self, count: usize) {
        let mut next_state = match self.state {
            LockState::Init => panic!(),
            LockState::Locked(_count, reset_at, unlock_at) => {
                LockState::Locked(count, reset_at, unlock_at)
            }
            LockState::Unlocked(count, reset_at) => {
                LockState::Unlocked(count, reset_at)
            }
        };
        std::mem::swap(&mut self.state, &mut next_state);
    }
    */
}

#[cfg(test)]
mod tests {
    use crate::credential::softlock::*;
    use crate::credential::totp::TOTP_DEFAULT_STEP;

    #[test]
    fn test_credential_softlock_statemachine() {
        // Check that given the set of inputs, correct decisions about
        // locking are made, and the states can be moved through.
        // ==> Check the init state.
        let mut slock = CredSoftLock::new(CredSoftLockPolicy::Password);
        assert!(slock.is_state_init());
        assert!(slock.is_valid());
        // A success does nothing, so we don't track them.
        let ct = Duration::from_secs(10);
        // Generate a failure
        // ==> trans to locked
        slock.record_failure(ct);
        assert!(
            slock.peek_state()
                == &LockState::Locked(1, Duration::from_secs(ONEDAY), Duration::from_secs(10 + 1))
        );
        // It will now fail
        // ==> trans ct < exp_at
        slock.apply_time_step(ct);
        assert!(!slock.is_valid());
        // A few seconds later it will be okay.
        // ==> trans ct < exp_at
        let ct2 = ct + Duration::from_secs(2);
        slock.apply_time_step(ct2);
        assert!(slock.is_valid());
        // Now trigger a failure now, we move back to locked.
        // ==> trans fail unlock -> lock
        slock.record_failure(ct2);
        assert!(
            slock.peek_state()
                == &LockState::Locked(2, Duration::from_secs(ONEDAY), Duration::from_secs(10 + 3))
        );
        assert!(!slock.is_valid());
        // Now check the reset_at behaviour. We need to check a locked and unlocked state.
        let mut slock2 = slock.clone();
        // This triggers the reset at from locked.
        // ==> trans locked -> init
        let ct3 = ct + Duration::from_secs(ONEDAY + 2);
        slock.apply_time_step(ct3);
        assert!(slock.is_state_init());
        assert!(slock.is_valid());
        // For slock2, we move to unlocked:
        // ==> trans unlocked -> init
        let ct4 = ct2 + Duration::from_secs(2);
        slock2.apply_time_step(ct4);
        eprintln!("{:?}", slock2.peek_state());
        assert!(slock2.peek_state() == &LockState::Unlocked(2, Duration::from_secs(ONEDAY)));
        slock2.apply_time_step(ct3);
        assert!(slock2.is_state_init());
        assert!(slock2.is_valid());
    }

    #[test]
    fn test_credential_softlock_policy_password() {
        let policy = CredSoftLockPolicy::Password;

        assert!(
            policy.failure_next_state(1, Duration::from_secs(0))
                == LockState::Locked(1, Duration::from_secs(ONEDAY), Duration::from_secs(1))
        );

        assert!(
            policy.failure_next_state(8, Duration::from_secs(0))
                == LockState::Locked(8, Duration::from_secs(ONEDAY), Duration::from_secs(3))
        );

        assert!(
            policy.failure_next_state(24, Duration::from_secs(0))
                == LockState::Locked(24, Duration::from_secs(ONEDAY), Duration::from_secs(5))
        );

        assert!(
            policy.failure_next_state(99, Duration::from_secs(0))
                == LockState::Locked(99, Duration::from_secs(ONEDAY), Duration::from_secs(10))
        );

        assert!(
            policy.failure_next_state(100, Duration::from_secs(0))
                == LockState::Locked(
                    100,
                    Duration::from_secs(ONEDAY),
                    Duration::from_secs(ONEDAY)
                )
        );
    }

    #[test]
    fn test_credential_softlock_policy_totp() {
        let policy = CredSoftLockPolicy::Totp(TOTP_DEFAULT_STEP);

        assert!(
            policy.failure_next_state(1, Duration::from_secs(10))
                == LockState::Locked(
                    1,
                    Duration::from_secs(TOTP_DEFAULT_STEP),
                    Duration::from_secs(11)
                )
        );

        assert!(
            policy.failure_next_state(2, Duration::from_secs(10))
                == LockState::Locked(
                    2,
                    Duration::from_secs(TOTP_DEFAULT_STEP),
                    Duration::from_secs(11)
                )
        );

        assert!(
            policy.failure_next_state(3, Duration::from_secs(10))
                == LockState::Locked(
                    3,
                    Duration::from_secs(TOTP_DEFAULT_STEP),
                    Duration::from_secs(TOTP_DEFAULT_STEP)
                )
        );
    }

    #[test]
    fn test_credential_softlock_policy_webauthn() {
        let policy = CredSoftLockPolicy::Webauthn;

        assert!(
            policy.failure_next_state(1, Duration::from_secs(0))
                == LockState::Locked(1, Duration::from_secs(1), Duration::from_secs(1))
        );

        // No matter how many failures, webauthn always only delays by 1 second.
        assert!(
            policy.failure_next_state(1000, Duration::from_secs(0))
                == LockState::Locked(1000, Duration::from_secs(1), Duration::from_secs(1))
        );
    }
}