use anyhow::{anyhow, Result};
use lazy_static::lazy_static;
use regex::Regex;
use std::fmt;
use std::ops::Deref;
use std::str::FromStr;
use std::time::SystemTime;

#[derive(Debug, PartialEq, Eq, Clone)]
pub enum DidOrHost {
    Did(String, String),
    Host(String),
}

impl FromStr for DidOrHost {
    type Err = anyhow::Error;

    /// DID syntax is specified in: <https://w3c.github.io/did-core/#did-syntax>
    ///
    /// Lazy partial hostname regex, isn't very correct.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        lazy_static! {
            static ref DID_RE: Regex =
                Regex::new(r"^did:([a-z]{1,64}):([a-zA-Z0-9\-.]{1,1024})$").unwrap();
        }
        lazy_static! {
            static ref HOSTNAME_RE: Regex =
                Regex::new(r"^[A-Za-z][A-Za-z0-9-]*(\.[A-Za-z][A-Za-z0-9-]*)+$").unwrap();
        }
        if let Some(caps) = DID_RE.captures(s) {
            Ok(Self::Did(caps[1].to_string(), caps[2].to_string()))
        } else if HOSTNAME_RE.is_match(s) {
            Ok(Self::Host(s.to_string()))
        } else {
            Err(anyhow!("does not match as a DID or hostname: {}", s))
        }
    }
}

impl fmt::Display for DidOrHost {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Host(v) => write!(f, "{}", v),
            Self::Did(m, v) => write!(f, "did:{}:{}", m, v),
        }
    }
}

#[test]
fn test_didorhost() {
    assert_eq!(
        DidOrHost::from_str("hyde.test").unwrap(),
        DidOrHost::Host("hyde.test".to_string())
    );
    assert_eq!(
        DidOrHost::from_str("did:method:blah").unwrap(),
        DidOrHost::Did("method".to_string(), "blah".to_string())
    );

    assert!(DidOrHost::from_str("barestring").is_err());
    assert!(DidOrHost::from_str("did:partial:").is_err());
    assert!(DidOrHost::from_str("").is_err());
    assert!(DidOrHost::from_str(" ").is_err());
    assert!(DidOrHost::from_str("1234").is_err());

    assert!(DidOrHost::from_str("multi.part.domain").is_ok());
    assert!(DidOrHost::from_str("did:is:weird").is_ok());
    assert!(DidOrHost::from_str("did:plc:bv6ggog3tya2z3vxsub7hnal").is_ok());
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct AtUri {
    pub repository: DidOrHost,
    pub collection: Option<String>,
    pub record: Option<String>,
    pub fragment: Option<String>,
}

impl FromStr for AtUri {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        lazy_static! {
            static ref ATURI_RE: Regex = Regex::new(r"^at://([a-zA-Z0-9:_\.-]+)(/(([a-zA-Z0-9\.]+))?)?(/(([a-zA-Z0-9\.-]+))?)?(#([a-zA-Z0-9/-]+))?$").unwrap();
        }
        if let Some(caps) = ATURI_RE.captures(s) {
            let uri = AtUri {
                repository: DidOrHost::from_str(&caps[1])?,
                collection: caps.get(4).map(|v| v.as_str().to_string()),
                record: caps.get(7).map(|v| v.as_str().to_string()),
                fragment: caps.get(9).map(|v| v.as_str().to_string()),
            };
            Ok(uri)
        } else {
            Err(anyhow!("couldn't parse as an at:// URI: {}", s))
        }
    }
}

impl fmt::Display for AtUri {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "at://{}", self.repository)?;
        if let Some(ref c) = self.collection {
            write!(f, "/{}", c)?;
        };
        if let Some(ref r) = self.record {
            write!(f, "/{}", r)?;
        };
        if let Some(ref v) = self.fragment {
            write!(f, "#{}", v)?;
        };
        Ok(())
    }
}

#[test]
fn test_aturi() {
    assert!(AtUri::from_str("at://bob.com").is_ok());
    assert!(AtUri::from_str("at://bob.com/").is_ok());
    assert!(AtUri::from_str("at://did:plc:bv6ggog3tya2z3vxsub7hnal").is_ok());
    assert!(AtUri::from_str("at://bob.com/io.example.song").is_ok());
    assert!(AtUri::from_str("at://bob.com/io.example.song/").is_ok());
    assert!(AtUri::from_str("at://bob.com/io.example.song/3yI5-c1z-cc2p-1a").is_ok());
    assert!(AtUri::from_str("at://bob.com/io.example.song/3yI5-c1z-cc2p-1a#/title").is_ok());
    assert!(
        AtUri::from_str("at://did:plc:ltk4reuh7rkoy2frnueetpb5/app.bsky.follow/3jg23pbmlhc2a")
            .is_ok()
    );

    let uri = AtUri {
        repository: DidOrHost::Did("some".to_string(), "thing".to_string()),
        collection: Some("com.atproto.record".to_string()),
        record: Some("asdf-123".to_string()),
        fragment: Some("/path".to_string()),
    };
    assert_eq!(
        "at://did:some:thing/com.atproto.record/asdf-123#/path",
        uri.to_string()
    );
    println!("{:?}", AtUri::from_str(&uri.to_string()));
    assert!(AtUri::from_str(&uri.to_string()).is_ok());

    let uri = AtUri::from_str("at://bob.com/io.example.song/3yI5-c1z-cc2p-1a#/title").unwrap();
    assert_eq!(uri.repository, DidOrHost::Host("bob.com".to_string()));
    assert_eq!(uri.collection, Some("io.example.song".to_string()));
    assert_eq!(uri.record, Some("3yI5-c1z-cc2p-1a".to_string()));
    assert_eq!(uri.fragment, Some("/title".to_string()));

    let uri = AtUri::from_str("at://bob.com/io.example.song/").unwrap();
    assert_eq!(uri.repository, DidOrHost::Host("bob.com".to_string()));
    assert_eq!(uri.collection, Some("io.example.song".to_string()));
}

/// A String (newtype) representing an NSID
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
pub struct Nsid(String);

impl FromStr for Nsid {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        lazy_static! {
            static ref NSID_RE: Regex = Regex::new(r"^([a-z][a-z0-9-]+\.)+[a-zA-Z0-9-]+$").unwrap();
        }
        if NSID_RE.is_match(s) {
            Ok(Self(s.to_string()))
        } else {
            Err(anyhow!("does not match as an NSID: {}", s))
        }
    }
}

impl Deref for Nsid {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl Nsid {
    pub fn domain(&self) -> String {
        self.rsplit('.').skip(1).collect::<Vec<&str>>().join(".")
    }

    pub fn name(&self) -> String {
        self.split('.')
            .last()
            .expect("multiple segments in NSID")
            .to_string()
    }
}

impl fmt::Display for Nsid {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

#[test]
fn test_nsid() {
    assert!(Nsid::from_str("com.atproto.recordType").is_ok());

    let nsid = Nsid::from_str("com.atproto.recordType").unwrap();
    assert_eq!(nsid.domain(), "atproto.com".to_string());
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
pub struct Did(String);

impl FromStr for Did {
    type Err = anyhow::Error;

    /// DID syntax is specified in: <https://w3c.github.io/did-core/#did-syntax>
    ///
    /// This regex does not follow that definition exactly.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        lazy_static! {
            static ref DID_RE: Regex =
                Regex::new(r"^did:([a-z]{1,32}):([a-zA-Z0-9\-.]{1,256})$").unwrap();
        }
        if DID_RE.is_match(s) {
            Ok(Self(s.to_string()))
        } else {
            Err(anyhow!("does not match as a DID: {}", s))
        }
    }
}

impl Deref for Did {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl Did {
    pub fn did_type(&self) -> String {
        self.split(':').nth(1).unwrap().to_string()
    }
}

impl fmt::Display for Did {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

#[test]
fn test_did() {
    assert!(Did::from_str("did:web:asdf.org").is_ok());
    assert!(Did::from_str("did:plc:asdf").is_ok());

    assert!(Did::from_str("bob.com").is_err());
    assert!(Did::from_str("").is_err());
    assert!(Did::from_str("did:").is_err());
    assert!(Did::from_str("did:plc:").is_err());
    assert!(Did::from_str("plc:asdf").is_err());
    assert!(Did::from_str("DID:thing:thang").is_err());

    assert_eq!(
        Did::from_str("did:web:asdf.org").unwrap().did_type(),
        "web".to_string()
    );
}

lazy_static! {
    /// Sortable base32 encoding, as bluesky implements/defines
    static ref BASE32SORT: data_encoding::Encoding = {
        let mut spec = data_encoding::Specification::new();
        spec.symbols.push_str("234567abcdefghijklmnopqrstuvwxyz");
        spec.padding = None;
        spec.encoding().unwrap()
    };
}

/// A string identifier for individual records, based on UNIX timestamp in microseconds.
///
/// See also: https://github.com/bluesky-social/atproto/issues/334
///
/// Pretty permissive about what can be parsed/accepted, because there were some old TIDs floating
/// around with weird format.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Tid(String);

impl FromStr for Tid {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        lazy_static! {
            static ref TID_RE: Regex = Regex::new(r"^[0-9a-zA-Z-]{13,20}$").unwrap();
        }
        if TID_RE.is_match(s) {
            Ok(Self(s.to_string()))
        } else {
            Err(anyhow!("does not match as a TID: {}", s))
        }
    }
}

impl Deref for Tid {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl fmt::Display for Tid {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl Tid {
    pub fn new(micros: u64, clock_id: u16) -> Self {
        // 53 bits of millis
        let micros = micros & 0x001FFFFFFFFFFFFF;
        // 10 bits of clock ID
        let clock_id = clock_id & 0x03FF;
        let val: u64 = (micros << 10) | (clock_id as u64);
        // big-endian encoding
        let enc = BASE32SORT.encode(&val.to_be_bytes());
        Tid(format!(
            "{}-{}-{}-{}",
            &enc[0..4],
            &enc[4..7],
            &enc[7..11],
            &enc[11..13]
        ))
    }
}

#[test]
fn test_tid() {
    Tid::from_str("3yI5-c1z-cc2p-1a").unwrap();
    assert!(Tid::from_str("3jg6anbimrc2a").is_ok());
    assert!(Tid::from_str("3yI5-c1z-cc2p-1a").is_ok());

    Tid::from_str("asdf234as4asdf234").unwrap();
    assert!(Tid::from_str("asdf234as4asdf234").is_ok());

    assert!(Tid::from_str("").is_err());
    assert!(Tid::from_str("com").is_err());
    assert!(Tid::from_str("com.blah.Thing").is_err());
    assert!(Tid::from_str("did:stuff:blah").is_err());

    let t1 = Tid::new(0, 0);
    assert_eq!(t1.to_string(), "2222-222-2222-22".to_string());
}

/// TID Generator
///
/// This version uses 53-bit microsecond counter (since UNIX epoch), and a random 10-bit clock id.
///
/// If the current timestamp is not greater than the last timestamp (either because clock did not
/// advance monotonically, or multiple TIDs were generated in the same microsecond (very unlikely),
/// the timestamp is simply incremented.
pub struct Ticker {
    last_timestamp: u64,
    clock_id: u16,
}

impl Ticker {
    pub fn new() -> Self {
        let mut ticker = Self {
            last_timestamp: 0,
            // mask to 10 bits
            clock_id: rand::random::<u16>() & 0x03FF,
        };
        // prime the pump
        ticker.next_tid();
        ticker
    }

    pub fn next_tid(&mut self) -> Tid {
        let now = SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)
            .expect("timestamp in micros since UNIX epoch")
            .as_micros() as u64;
        // mask to 53 bits
        let now = now & 0x001FFFFFFFFFFFFF;
        if now > self.last_timestamp {
            self.last_timestamp = now;
        } else {
            self.last_timestamp += 1;
        }
        Tid::new(self.last_timestamp, self.clock_id)
    }
}

impl Default for Ticker {
    fn default() -> Self {
        Self::new()
    }
}

#[test]
fn test_ticker() {
    let mut ticker = Ticker::new();
    let mut prev = ticker.next_tid();
    let mut next = ticker.next_tid();
    for _ in [0..100] {
        println!("{} >? {}", next, prev);
        assert!(next > prev);
        prev = next;
        next = ticker.next_tid();
    }
    println!("{}", prev);
    assert_eq!(prev, Tid::from_str(&prev.to_string()).unwrap());
    assert_eq!(next[13..16], prev[13..16]);

    let mut other_ticker = Ticker::new();
    let other = other_ticker.next_tid();
    assert!(other > next);
    assert!(next[13..16] != other[13..16]);
}