use crate::consts::{CLIENT, MAX_INFO_LENGTH};
use crate::tracker::Tracker;
use crate::types::{InfoHash, InfoHashProvider};
use bendy::decoding::{Decoder, Object};
use bendy::encoding::{SingleItemEncoder, ToBencode};
use bytes::{BufMut, Bytes, BytesMut};
use patharg::OutputArg;
use sha1::{Digest, Sha1};
use std::borrow::Cow;
use std::fmt::Write;
use std::iter::Peekable;
use std::net::SocketAddr;
use std::ops::Range;
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use thiserror::Error;
use tokio::fs::create_dir_all;

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct TorrentInfo<H> {
    info_hash: H,
    data: Bytes,
}

impl<H> TorrentInfo<H> {
    pub(crate) fn name(&self) -> Option<Cow<'_, str>> {
        let mut decoder = Decoder::new(&self.data);
        let Ok(Some(obj)) = decoder.next_object() else {
            unreachable!();
        };
        let mut dd = obj
            .try_into_dictionary()
            .expect("Torrent info should be a dict");
        while let Some(kv) = dd.next_pair().ok()? {
            if let (b"name", v) = kv {
                return Some(String::from_utf8_lossy(v.try_into_bytes().ok()?));
            }
        }
        None
    }
}

impl<H> From<TorrentInfo<H>> for Bytes {
    fn from(info: TorrentInfo<H>) -> Bytes {
        info.data
    }
}

#[derive(Clone, Debug)]
pub(crate) struct TorrentInfoBuilder<H> {
    info_hash: H,
    hasher: Sha1,
    data: BytesMut,
    sizes: Vec<usize>,
    index_iter: Peekable<Range<u32>>,
}

impl<H: InfoHashProvider> TorrentInfoBuilder<H> {
    const PIECE_LENGTH: usize = 16 << 10; // 16 KiB

    pub(crate) fn new(info_hash: H, length: u32) -> Result<TorrentInfoBuilder<H>, ConstructError> {
        let Ok(lgth) = usize::try_from(length) else {
            return Err(ConstructError::TooLarge(length));
        };
        if lgth > MAX_INFO_LENGTH {
            return Err(ConstructError::TooLarge(length));
        }
        let hasher = Sha1::new();
        let data = BytesMut::with_capacity(lgth);
        let mut sizes = vec![Self::PIECE_LENGTH; lgth / Self::PIECE_LENGTH];
        let overflow = lgth % Self::PIECE_LENGTH;
        if overflow > 0 {
            sizes.push(overflow);
        }
        let piece_qty =
            u32::try_from(sizes.len()).expect("number of metadata pieces should fit in a u32");
        Ok(TorrentInfoBuilder {
            info_hash,
            hasher,
            data,
            sizes,
            index_iter: (0..piece_qty).peekable(),
        })
    }

    pub(crate) fn push(&mut self, piece: Bytes) -> Result<(), PushError> {
        let Some(index) = self.index_iter.next() else {
            return Err(PushError::TooManyPieces);
        };
        let index = usize::try_from(index).expect("piece indices should fit in a usize");
        if piece.len() != self.sizes[index] {
            return Err(PushError::Length {
                index,
                expected: self.sizes[index],
                got: piece.len(),
            });
        }
        self.hasher.update(&piece);
        self.data.extend(piece);
        Ok(())
    }

    pub(crate) fn next_piece(&mut self) -> Option<u32> {
        self.index_iter.peek().copied()
    }

    pub(crate) fn build(self) -> Result<TorrentInfo<H>, BuildError> {
        let left = self.index_iter.count();
        if left > 0 {
            return Err(BuildError::NotFinished { left });
        }
        let got_hash = Bytes::from_iter(self.hasher.finalize());
        if got_hash != self.info_hash.get_info_hash().as_bytes() {
            return Err(BuildError::Digest {
                expected: self.info_hash.get_info_hash(),
                got: got_hash,
            });
        }
        let data = self.data.freeze();
        check_bencode_dict(&data)?;
        Ok(TorrentInfo {
            info_hash: self.info_hash,
            data,
        })
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct TorrentFile<H> {
    info: TorrentInfo<H>,
    trackers: Vec<Arc<Tracker>>,
    nodes: Vec<SocketAddr>,
    creation_date: i64,
    created_by: String,
}

impl<H: InfoHashProvider> TorrentFile<H> {
    pub(crate) fn new(
        info: TorrentInfo<H>,
        trackers: Vec<Arc<Tracker>>,
        nodes: Vec<SocketAddr>,
    ) -> TorrentFile<H> {
        TorrentFile {
            trackers,
            nodes,
            created_by: CLIENT.into(),
            creation_date: unix_now(),
            info,
        }
    }

    pub(crate) async fn save(self, template: &PathTemplate) -> std::io::Result<()> {
        let name = sanitize(self.info.name().as_deref().unwrap_or("NONAME"));
        let path = OutputArg::from_arg(template.format(&name, self.info.info_hash.get_info_hash()));
        if path.is_stdout() {
            log::info!(
                "Writing torrent for info hash {} to stdout",
                self.info.info_hash
            );
        } else {
            log::info!(
                "Saving torrent for info hash {} to file {}",
                self.info.info_hash,
                path
            );
        }
        if let Some(parent) = path.path_ref().and_then(|p| p.parent())
            && parent != Path::new("")
        {
            create_dir_all(parent).await?;
        }
        let buf = Bytes::from(self);
        path.async_write(buf).await
    }
}

macro_rules! put_kv {
    ($buf:ident, $key:literal, $value:expr) => {
        $buf.put(
            $key.to_bencode()
                .expect("string keys should be bencodable")
                .as_slice(),
        );
        $buf.put(
            $value
                .to_bencode()
                .expect("torrent file values should be bencodable")
                .as_slice(),
        );
    };
}

impl<H> From<TorrentFile<H>> for Bytes {
    fn from(torrent: TorrentFile<H>) -> Bytes {
        let mut buf = BytesMut::new();
        buf.put_u8(b'd');
        if !torrent.trackers.is_empty() {
            put_kv!(
                buf,
                "announce-list",
                torrent
                    .trackers
                    .into_iter()
                    .map(|tr| vec![tr.url_string()])
                    .collect::<Vec<Vec<String>>>()
            );
        }
        put_kv!(buf, "created by", torrent.created_by);
        put_kv!(buf, "creation date", torrent.creation_date);
        buf.put(
            "info"
                .to_bencode()
                .expect("string should be bencodable")
                .as_slice(),
        );
        buf.put(Bytes::from(torrent.info));
        if !torrent.nodes.is_empty() {
            put_kv!(
                buf,
                "nodes",
                torrent.nodes.into_iter().map(NodeAddr).collect::<Vec<_>>()
            );
        }
        buf.put_u8(b'e');
        buf.freeze()
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct PathTemplate(Vec<TemplateElement>);

impl PathTemplate {
    pub(crate) fn format(&self, name: &str, info_hash: InfoHash) -> String {
        let mut buf = String::new();
        for elem in &self.0 {
            match elem {
                TemplateElement::Literal(s) => buf.push_str(s),
                TemplateElement::Name => buf.push_str(name),
                TemplateElement::Hash => {
                    write!(buf, "{info_hash}").expect("fmt::writing to a String should not fail");
                }
            }
        }
        buf
    }
}

impl FromStr for PathTemplate {
    type Err = PathTemplateError;

    fn from_str(s: &str) -> Result<PathTemplate, PathTemplateError> {
        let mut elems = Vec::new();
        let mut buf = String::new();
        let mut brace_iter = s.match_indices('{');
        let mut prev_end = 0;
        while let Some((i, _)) = brace_iter.next() {
            debug_assert!(
                prev_end <= i,
                "prev_end={prev_end:?} was unexpectedly greater than i={i:?}"
            );
            buf.push_str(&s[prev_end..i]);
            match s[i..]
                .char_indices()
                .skip(1)
                .find(|&(_, ch)| !(ch.is_ascii_alphanumeric() || ch == '_'))
            {
                Some((1, '{')) => {
                    buf.push('{');
                    let _ = brace_iter.next();
                    prev_end = i + 2;
                }
                Some((j, '}')) => {
                    if !buf.is_empty() {
                        elems.push(TemplateElement::Literal(buf.replace("}}", "}")));
                        buf.clear();
                    }
                    match &s[(i + 1)..(i + j)] {
                        "name" => elems.push(TemplateElement::Name),
                        "hash" => elems.push(TemplateElement::Hash),
                        field => return Err(PathTemplateError::UnknownField(field.into())),
                    }
                    prev_end = i + j + 1;
                }
                Some(_) => return Err(PathTemplateError::InvalidField(i)),
                None => return Err(PathTemplateError::Unmatched(i)),
            }
        }
        buf.push_str(&s[prev_end..]);
        if !buf.is_empty() {
            elems.push(TemplateElement::Literal(buf.replace("}}", "}")));
        }
        Ok(PathTemplate(elems))
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
enum TemplateElement {
    Literal(String),
    Name,
    Hash,
}

#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub(crate) enum PathTemplateError {
    #[error("unmatched brace at byte index {0}")]
    Unmatched(usize),
    #[error("malformed placeholder at byte index {0}")]
    InvalidField(usize),
    #[error("unknown placeholder {0:?}")]
    UnknownField(String),
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct NodeAddr(SocketAddr);

impl ToBencode for NodeAddr {
    const MAX_DEPTH: usize = 1;

    fn encode(&self, encoder: SingleItemEncoder<'_>) -> Result<(), bendy::encoding::Error> {
        encoder.emit_list(|lst| {
            lst.emit_str(&self.0.ip().to_string())?;
            lst.emit_int(self.0.port())?;
            Ok(())
        })
    }
}

fn check_bencode_dict(buf: &Bytes) -> Result<(), BencodeDictError> {
    let mut decoder = Decoder::new(buf);
    match decoder.next_object() {
        Ok(Some(Object::Dict(mut dd))) => {
            if dd.consume_all().is_err() {
                return Err(BencodeDictError::Syntax);
            }
        }
        Ok(Some(_)) => return Err(BencodeDictError::NotADict),
        Ok(None) => return Err(BencodeDictError::Empty),
        Err(_) => return Err(BencodeDictError::Syntax),
    }
    if !matches!(decoder.next_object(), Ok(None)) {
        return Err(BencodeDictError::Trailing);
    }
    Ok(())
}

fn unix_now() -> i64 {
    match SystemTime::now().duration_since(UNIX_EPOCH) {
        Ok(d) => i64::try_from(d.as_secs()).unwrap_or(i64::MAX),
        Err(e) => i64::try_from(e.duration().as_secs())
            .map(|i| -i)
            .unwrap_or(i64::MIN),
    }
}

fn sanitize(s: &str) -> String {
    static PRINTABLE_UNSANITARY: &str = "/\\<>:|\"?*";
    s.chars()
        .map(|ch| {
            if ch < ' ' || PRINTABLE_UNSANITARY.contains(ch) {
                '_'
            } else {
                ch
            }
        })
        .collect()
}

#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)]
pub(crate) enum ConstructError {
    #[error("metadata size of {0} exceeds {max} limit", max = MAX_INFO_LENGTH)]
    TooLarge(u32),
}

#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)]
pub(crate) enum PushError {
    #[error("too many metadata pieces fetched")]
    TooManyPieces,
    #[error("wrong length for metadata piece {index}: expected {expected}, got {got}")]
    Length {
        index: usize,
        expected: usize,
        got: usize,
    },
}

#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub(crate) enum BuildError {
    #[error(transparent)]
    Bencode(#[from] BencodeDictError),
    #[error("not all metadata pieces fetched; {left} remaining")]
    NotFinished { left: usize },
    #[error("info hash mismatch: expected {expected}, got {got:x}")]
    Digest { expected: InfoHash, got: Bytes },
}

#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)]
pub(crate) enum BencodeDictError {
    #[error("data is not valid bencode")]
    Syntax,
    #[error("data is not a bencode dict")]
    NotADict,
    #[error("data is empty")]
    Empty,
    #[error("data has trailing bytes after bencode dict")]
    Trailing,
}

#[cfg(test)]
mod tests {
    use super::*;

    mod check_bencode_dict {
        use super::*;

        #[test]
        fn good() {
            let buf = Bytes::from(b"d3:foo3:bar3:keyi42ee".as_slice());
            assert_eq!(check_bencode_dict(&buf), Ok(()));
        }

        #[test]
        fn invalid_bencode() {
            let buf = Bytes::from(b"d3:keyi42e8:no valuee".as_slice());
            assert_eq!(check_bencode_dict(&buf), Err(BencodeDictError::Syntax));
        }

        #[test]
        fn non_dict() {
            let buf = Bytes::from(b"l3:keyi42e8:no valuee".as_slice());
            assert_eq!(check_bencode_dict(&buf), Err(BencodeDictError::NotADict));
        }

        #[test]
        fn empty() {
            let buf = Bytes::new();
            assert_eq!(check_bencode_dict(&buf), Err(BencodeDictError::Empty));
        }

        #[test]
        fn trailing_bencode() {
            let buf = Bytes::from(b"d3:foo3:bar3:keyi42ee5:extra".as_slice());
            assert_eq!(check_bencode_dict(&buf), Err(BencodeDictError::Trailing));
        }
    }

    #[test]
    fn test_torrent_info_builder() {
        let metadata_size = 40 << 10;
        let info_hash = "fd33560457eae4b165bc5e7f7de6f24db61e957e"
            .parse::<InfoHash>()
            .unwrap();
        let mut builder = TorrentInfoBuilder::new(info_hash, metadata_size).unwrap();
        assert_eq!(builder.next_piece(), Some(0));
        let mut piece0 = BytesMut::with_capacity(16 << 10);
        piece0.put(b"d4:name15:My Test Torrent11:xtra-filler40914:".as_slice());
        piece0.put_bytes(0, 16339);
        builder.push(piece0.freeze()).unwrap();
        assert_eq!(builder.next_piece(), Some(1));
        builder.push(BytesMut::zeroed(16 << 10).freeze()).unwrap();
        assert_eq!(builder.next_piece(), Some(2));
        let mut piece2 = BytesMut::with_capacity(8 << 10);
        piece2.put_bytes(0, (8 << 10) - 1);
        piece2.put_u8(b'e');
        builder.push(piece2.freeze()).unwrap();
        assert_eq!(builder.next_piece(), None);
        let info = builder.build().unwrap();
        assert_eq!(info.name(), Some(Cow::from("My Test Torrent")));
    }

    #[test]
    fn torrent_file_into_bytes() {
        let info = TorrentInfo {
            info_hash: "ddbf90f0d41c8f91a555192279845bc45e530ec9".parse::<InfoHash>().unwrap(),
            data: Bytes::from(b"d6:lengthi42e4:name8:blob.dat12:piece lengthi65535e6:pieces20:00000000000000000000e".as_slice()),
        };
        let torrent = TorrentFile {
            info,
            trackers: vec![
                "http://tracker.example.com:8080/announce"
                    .parse::<Tracker>()
                    .unwrap()
                    .into(),
                "udp://bits.example.net:9001"
                    .parse::<Tracker>()
                    .unwrap()
                    .into(),
            ],
            nodes: Vec::new(),
            creation_date: 1686939764,
            created_by: "demagnetize vDEV".into(),
        };
        let buf = Bytes::from(torrent);
        assert_eq!(buf, b"d13:announce-listll40:http://tracker.example.com:8080/announceel27:udp://bits.example.net:9001ee10:created by16:demagnetize vDEV13:creation datei1686939764e4:infod6:lengthi42e4:name8:blob.dat12:piece lengthi65535e6:pieces20:00000000000000000000ee".as_slice());
        check_bencode_dict(&buf).unwrap();
    }

    #[test]
    fn torrent_file_with_nodes_into_bytes() {
        let info = TorrentInfo {
            info_hash: "ddbf90f0d41c8f91a555192279845bc45e530ec9".parse::<InfoHash>().unwrap(),
            data: Bytes::from(b"d6:lengthi42e4:name8:blob.dat12:piece lengthi65535e6:pieces20:00000000000000000000e".as_slice()),
        };
        let torrent = TorrentFile {
            info,
            trackers: Vec::new(),
            nodes: vec![
                "127.0.0.1:8080".parse::<SocketAddr>().unwrap(),
                "10.1.2.3:60069".parse::<SocketAddr>().unwrap(),
                "[2001::ffff:c0a8:ff]:65535".parse::<SocketAddr>().unwrap(),
            ],
            creation_date: 1686939764,
            created_by: "demagnetize vDEV".into(),
        };
        let buf = Bytes::from(torrent);
        assert_eq!(buf, b"d10:created by16:demagnetize vDEV13:creation datei1686939764e4:infod6:lengthi42e4:name8:blob.dat12:piece lengthi65535e6:pieces20:00000000000000000000e5:nodesll9:127.0.0.1i8080eel8:10.1.2.3i60069eel18:2001::ffff:c0a8:ffi65535eeee".as_slice());
        check_bencode_dict(&buf).unwrap();
    }

    mod path_template {
        use super::*;

        #[test]
        fn basic() {
            let template = "Torrent-{name}-{hash}.torrent"
                .parse::<PathTemplate>()
                .unwrap();
            let info_hash = "ddbf90f0d41c8f91a555192279845bc45e530ec9"
                .parse::<InfoHash>()
                .unwrap();
            assert_eq!(
                template.format("My Test Torrent", info_hash),
                "Torrent-My Test Torrent-ddbf90f0d41c8f91a555192279845bc45e530ec9.torrent"
            );
        }

        #[test]
        fn escaped_braces() {
            let template = "Torrent-{{{name}}}-{hash}.torrent"
                .parse::<PathTemplate>()
                .unwrap();
            let info_hash = "ddbf90f0d41c8f91a555192279845bc45e530ec9"
                .parse::<InfoHash>()
                .unwrap();
            assert_eq!(
                template.format("My Test Torrent", info_hash),
                "Torrent-{My Test Torrent}-ddbf90f0d41c8f91a555192279845bc45e530ec9.torrent"
            );
        }

        #[test]
        fn no_leading_or_trailing_literals() {
            let template = "{name}-{hash}".parse::<PathTemplate>().unwrap();
            let info_hash = "ddbf90f0d41c8f91a555192279845bc45e530ec9"
                .parse::<InfoHash>()
                .unwrap();
            assert_eq!(
                template.format("My Test Torrent", info_hash),
                "My Test Torrent-ddbf90f0d41c8f91a555192279845bc45e530ec9"
            );
        }

        #[test]
        fn unmatched() {
            let e = "torrent={name".parse::<PathTemplate>().unwrap_err();
            assert_eq!(e, PathTemplateError::Unmatched(8));
            assert_eq!(e.to_string(), "unmatched brace at byte index 8");
        }

        #[test]
        fn nested_field() {
            let e = "{name{hash}torrent}".parse::<PathTemplate>().unwrap_err();
            assert_eq!(e, PathTemplateError::InvalidField(0));
            assert_eq!(e.to_string(), "malformed placeholder at byte index 0");
        }

        #[test]
        fn invalid_field() {
            let e = "torrent={name+hash}".parse::<PathTemplate>().unwrap_err();
            assert_eq!(e, PathTemplateError::InvalidField(8));
            assert_eq!(e.to_string(), "malformed placeholder at byte index 8");
        }

        #[test]
        fn unknown_field() {
            let e = "torrent={tracker}.torrent"
                .parse::<PathTemplate>()
                .unwrap_err();
            assert_eq!(e, PathTemplateError::UnknownField("tracker".into()));
            assert_eq!(e.to_string(), "unknown placeholder \"tracker\"");
        }
    }
}
