use std::error::Error;
use std::fmt::Formatter;
use std::str::FromStr;

use regex::Regex;
use serde::Serializer;

#[derive(Debug, Clone)]
pub enum PackageIdentifierError {
    InvalidPackageLocator(String),
    InvalidURI(String),
    InvalidPath(String),
}

impl std::fmt::Display for PackageIdentifierError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            PackageIdentifierError::InvalidPackageLocator(s) => write!(f, "Invalid package locator: {}", s),
            PackageIdentifierError::InvalidURI(s) => write!(f, "Invalid URI: {}", s),
            PackageIdentifierError::InvalidPath(s) => write!(f, "Invalid path: {}", s),
        }
    }
}

impl Error for PackageIdentifierError {}

#[derive(Clone, Eq, PartialEq)]
pub enum PackageIdentifier {
    PackageLocator(PackageLocator),
    URI(String),
    Path(String)
}

impl std::fmt::Debug for PackageIdentifier {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        if f.alternate() {
            match self {
                PackageIdentifier::PackageLocator(pl) => write!(f, "PackageLocator({:#?})", pl),
                PackageIdentifier::URI(uri) => write!(f, "URI({:?})", uri),
                PackageIdentifier::Path(path) => write!(f, "Path({:?})", path),
            }
        } else {
            match self {
                PackageIdentifier::PackageLocator(pl) => write!(f, "PL: {:?}", pl),
                PackageIdentifier::URI(uri) => write!(f, "URI: {:?}", uri),
                PackageIdentifier::Path(path) => write!(f, "Path: {:?}", path),
            }
        }
    }
}

impl std::fmt::Display for PackageIdentifier {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            PackageIdentifier::PackageLocator(pl) => write!(f, "{}", pl),
            PackageIdentifier::URI(uri) => write!(f, "{}", uri),
            PackageIdentifier::Path(path) => write!(f, "{}", path),
        }
    }
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct PackageLocator {
    pub name: String,
    pub version: Option<u32>,
    pub tags: Option<Vec<String>>,
}

impl std::fmt::Display for PackageLocator {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        let mut s = self.name.clone();
        if let Some(version) = self.version {
            s += &*format!("@{}", version);
        }
        if let Some(tags) = &self.tags {
            s += &*format!(":{}", tags.join(","));
        }
        write!(f, "{}", s)
    }
}

impl FromStr for PackageIdentifier {
    type Err = PackageIdentifierError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let uri_re = Regex::new(r"^[a-zA-Z0-9]+://").unwrap();
        if uri_re.is_match(s) {
            // there needs to be stuff after the protocol
            let split = s.split("://").collect::<Vec<&str>>();
            if split.len() != 2 || split[1].is_empty() {
                return Err(PackageIdentifierError::InvalidURI(s.to_string()));
            }
            Ok(PackageIdentifier::URI(s.to_string()))
        } else if s.starts_with("/")
            || s.starts_with("./")
            || s.starts_with("../")
            || s.starts_with("~/")
        {
            let path = std::path::Path::new(s);
            if s.ends_with("/")
                || !path.exists()
                || path.is_dir()
            {
                return Err(PackageIdentifierError::InvalidPath(s.to_string()));
            }
            return Ok(PackageIdentifier::Path(s.to_string()));
        } else {
            let pl = match PackageLocator::from_str(s) {
                Ok(pl) => pl,
                Err(e) => return Err(PackageIdentifierError::InvalidPackageLocator(e.to_string())),
            };
            Ok(PackageIdentifier::PackageLocator(pl))
        }
    }
}

impl FromStr for PackageLocator {
    type Err = PackageIdentifierError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        #[allow(unused_assignments)] // false positive
        let mut name = None;
        let mut version = None;
        let mut tags = None;

        if s.is_empty() {
            return Err(PackageIdentifierError::InvalidPackageLocator(s.to_string()));
        }

        let name_re = Regex::new("^([A-Za-z][A-Za-z0-9_.]+)").unwrap();
        let version_re = Regex::new("@([0-9]+)").unwrap();
        let tags_re = Regex::new(":([a-zA-Z0-9,._]+)").unwrap();

        if let Some(caps) = name_re.captures(s) {
            name = Some(caps.get(1).unwrap().as_str().to_string());
        } else {
            return Err(PackageIdentifierError::InvalidPackageLocator(s.to_string()));
        }

        if let Some(caps) = version_re.captures(s) {
            version = Some(caps.get(1).unwrap().as_str().parse::<u32>().unwrap());
        }

        if let Some(caps) = tags_re.captures(s) {
            tags = Some(
                caps.get(1)
                    .unwrap()
                    .as_str()
                    .split(",")
                    .map(|s| s.to_string())
                    .collect(),
            );
        }

        Ok(PackageLocator {
            name: name.unwrap(),
            version,
            tags,
        })
    }
}

impl TryFrom<(String, String)> for PackageLocator {
    type Error = PackageIdentifierError;
    fn try_from((name, locate_str): (String, String)) -> Result<PackageLocator, Self::Error> {
        // name = "pkg"
        // locate_str = "1.0.0:tag1,tag2" or "1.0.0" or "tag1,tag2"
        let mut version = None;
        let mut tags = None;

        let version_re = Regex::new("^([0-9]+)").unwrap();
        let tags_re = Regex::new("^:([a-zA-Z0-9,._]+)").unwrap();

        if let Some(caps) = version_re.captures(locate_str.as_str()) {
            version = Some(caps.get(1).unwrap().as_str().parse::<u32>().unwrap());
        }

        if let Some(caps) = tags_re.captures(locate_str.as_str()) {
            tags = Some(
                caps.get(1)
                    .unwrap()
                    .as_str()
                    .split(",")
                    .map(|s| s.to_string())
                    .collect(),
            );
        }

        Ok(PackageLocator {
            name,
            version,
            tags,
        })
    }
}