use std::time::{SystemTime, Duration};

use crate::traits::IdGeneratorMut;
use crate::error;
use crate::flake::Snowflake;
use crate::cloud::common::{Counts, NANOSECOND, MILLI_IN_SECOND};

/// non-thread safe generator
#[derive(Clone)]
pub struct SingleThread<const TS: u8, const PID: u8, const SEQ: u8> {
    ep: SystemTime,
    pid: i64,
    counts: Counts,
}

impl<const TS: u8, const PID: u8, const SEQ: u8> SingleThread<TS, PID, SEQ> {
    /// max timestamp that can be provided to the struct. based on TS
    pub const MAX_TIMESTAMP: u64 = (1 << TS as u64) - 1;
    /// max primary id that can be provided to the struct. based on PID
    pub const MAX_PRIMARY_ID: i64 = (1 << PID as i64) - 1;
    /// max sequence that can be generated by the struct. based on SEQ
    pub const MAX_SEQUENCE: i64 = (1 << SEQ as i64) - 1;

    /// max duration possible when generating EPOCH durations. based on TS
    pub const MAX_DURATION: Duration = Duration::from_millis((1 << TS as u64) - 1);

    /// returns a new SingleThread generator
    ///
    /// will return an error if the primary id is invalid, the timestamp is
    /// invalid, it failes to retrieve the current timestamp, or if the epoch
    /// is ahead of the current timestamp
    pub fn new(pid: i64, epoch: u64) -> error::Result<Self> {
        if pid < 0 || pid > Self::MAX_PRIMARY_ID {
            return Err(error::Error::PrimaryIdInvalid);
        }

        if epoch > Self::MAX_TIMESTAMP {
            return Err(error::Error::EpochInvalid);
        }

        let Some(sys_time) = SystemTime::UNIX_EPOCH.clone()
            .checked_add(Duration::from_millis(epoch)) else {
            return Err(error::Error::TimestampError);
        };
        let prev_time = sys_time.elapsed()?;

        Ok(SingleThread {
            ep: sys_time,
            pid,
            counts: Counts {
                sequence: 1,
                prev_time,
            }
        })
    }

    /// returns epoch
    pub fn epoch(&self) -> &SystemTime {
        &self.ep
    }

    /// returns primary id
    pub fn primary_id(&self) -> &i64 {
        &self.pid
    }

    /// retrieves the next available id
    ///
    /// if the current timestamp reaches max, the max sequence value is
    /// reached, or if it fails to get the current timestamp this will return
    /// an error
    pub fn next_id(&mut self) -> error::Result<Snowflake<TS, PID, SEQ>> {
        let seq: i64;

        let ts = self.ep.elapsed()?;

        if ts > Self::MAX_DURATION {
            return Err(error::Error::TimestampMaxReached);
        }

        let ts_secs = ts.as_secs();
        let ts_nanos = ts.subsec_nanos();
        let ts_millis = ts_nanos / NANOSECOND;
        let prev_millis = self.counts.prev_time.subsec_nanos() / NANOSECOND;

        let tsm = (ts_secs as i64) * MILLI_IN_SECOND + (ts_millis as i64);

        if self.counts.prev_time.as_secs() == ts_secs && prev_millis == ts_millis {
            seq = self.counts.sequence;

            if seq > Self::MAX_SEQUENCE {
                return Err(error::Error::SequenceMaxReached(
                    Duration::from_nanos((NANOSECOND - (ts_nanos % NANOSECOND)) as u64)
                ));
            }

            self.counts.sequence += 1;
        } else {
            seq = 1;

            self.counts.prev_time = ts;
            self.counts.sequence = 2;
        }

        Ok(Snowflake {
            ts,
            tsm,
            pid: self.pid,
            seq,
        })
    }
}

impl<const TS: u8, const PID: u8, const SEQ: u8> IdGeneratorMut for SingleThread<TS, PID, SEQ> {
    type Error = error::Error;
    type Id = Snowflake<TS, PID, SEQ>;
    type Output = std::result::Result<Self::Id, Self::Error>;

    fn next_id(&mut self) -> Self::Output {
        SingleThread::next_id(self)
    }
}

#[cfg(test)]
mod test {
    use std::collections::HashMap;
    use std::io::Write as _;

    use super::*;
    use crate::flake::Snowflake;

    const START_TIME: u64 = 1679082337000;
    const MACHINE_ID: i64 = 1;

    type TestSnowflake = Snowflake<43, 8, 12>;
    type TestSnowcloud = SingleThread<43, 8, 12>;

    #[test]
    fn unique_ids() -> () {
        let mut cloud = TestSnowcloud::new(MACHINE_ID, START_TIME).unwrap();
        let mut found_dups = false;
        let mut total_found: usize = 0;
        let mut unique_ids: HashMap<i64, Vec<(usize, TestSnowflake)>> = HashMap::new();
        let mut generated: Vec<TestSnowflake> = Vec::with_capacity(TestSnowcloud::MAX_SEQUENCE as usize);

        for _ in 0..generated.capacity() {
            generated.push(cloud.next_id().expect("failed next_id"));
        }

        for i in 0..generated.len() {
            let flake = &generated[i];
            let id: i64 = flake.id();

            if let Some(dups) = unique_ids.get_mut(&id) {
                found_dups = true;
                total_found += 1;

                dups.push((i, flake.clone()));
            } else {
                let mut dups = Vec::with_capacity(1);
                dups.push((i, flake.clone()));

                unique_ids.insert(id, dups);
            }
        }

        if !found_dups {
            return;
        }

        let seq_width = (TestSnowcloud::MAX_SEQUENCE.checked_ilog10().unwrap_or(0) + 1) as usize;
        let index_width = (generated.len().checked_ilog10().unwrap_or(0) + 1) as usize;
        let mut debug_output = std::fs::OpenOptions::new()
            .create(true)
            .write(true)
            .truncate(true)
            .open("SingleThread_unique_id.debug.txt")
            .expect("failed to create debug_file");

        debug_output.write_fmt(format_args!("total found: {} / {}\n", total_found, generated.len())).unwrap();

        for flake in &generated {
            let id = flake.id();

            if let Some(dups) = unique_ids.get(&id) {
                if dups.len() > 1 {
                    total_found += 1;

                    debug_output.write_fmt(format_args!(
                        "flake: {}\n",
                        id,
                    )).unwrap();

                    for dup in dups {
                        debug_output.write_fmt(format_args!(
                            "index: {:index_width$} {} {} {:seq_width$} | {}.{}\n",
                            dup.0,
                            dup.1.timestamp(),
                            dup.1.primary_id(),
                            dup.1.sequence(),
                            dup.1.duration().as_secs(),
                            dup.1.duration().subsec_nanos(),
                            index_width = index_width,
                            seq_width = seq_width,
                        )).unwrap();
                    }
                }
            }
        }

        debug_output.write(b"\n").unwrap();

        for index in 0..generated.len() {
            let mut is_dup = false;
            let id = generated[index].id();

            if let Some(dups) = unique_ids.get(&id) {
                is_dup = dups.len() > 1;
            }

            debug_output.write_fmt(format_args!(
                "{:index_width$} {} {} {:seq_width$} | {}.{} {}\n",
                index,
                generated[index].timestamp(),
                generated[index].primary_id(),
                generated[index].sequence(),
                generated[index].duration().as_secs(),
                generated[index].duration().subsec_nanos(),
                if is_dup { 'd' } else { ' ' },
                index_width = index_width,
                seq_width = seq_width,
            )).unwrap();
        }

        panic!("encountered duplidate ids. check SingleThread_unique_id.debug.txt for details"); 
    }
}
