use crate::{blueprint::error as blueprint, deps::manifest::Package, package_name::PackageName};
use aiken_lang::{
    ast::{self, Span},
    error::ExtraData,
    parser::error::ParseError,
    tipo,
};
use miette::{
    Diagnostic, EyreContext, LabeledSpan, MietteHandlerOpts, NamedSource, RgbColors, SourceCode,
};
use owo_colors::{OwoColorize, Stream::Stdout};
use std::{
    fmt::{Debug, Display},
    io,
    ops::Deref,
    path::{Path, PathBuf},
};
use zip::result::ZipError;

#[allow(dead_code)]
#[derive(thiserror::Error)]
pub enum Error {
    #[error("I just found two modules with the same name: '{module}'")]
    DuplicateModule {
        module: String,
        first: PathBuf,
        second: PathBuf,
    },

    #[error("Some operation on the file-system did fail.")]
    FileIo { error: io::Error, path: PathBuf },

    #[error("I found some files with incorrectly formatted source code.")]
    Format { problem_files: Vec<Unformatted> },

    #[error(transparent)]
    Blueprint(#[from] blueprint::Error),

    #[error(transparent)]
    StandardIo(#[from] io::Error),

    #[error(transparent)]
    Http(#[from] reqwest::Error),

    #[error(transparent)]
    ZipExtract(#[from] ZipError),

    #[error(transparent)]
    JoinError(#[from] tokio::task::JoinError),

    #[error(transparent)]
    Json(#[from] serde_json::Error),

    #[error(transparent)]
    Module(#[from] ast::Error),

    #[error("{help}")]
    TomlLoading {
        path: PathBuf,
        src: String,
        named: Box<NamedSource>,
        location: Option<Span>,
        help: String,
    },

    #[error("I couldn't find any 'aiken.toml' manifest in {path}.")]
    MissingManifest { path: PathBuf },

    #[error("I just found a cycle in module hierarchy!")]
    ImportCycle { modules: Vec<String> },

    #[error("While parsing files...")]
    Parse {
        path: PathBuf,
        src: String,
        named: NamedSource,
        #[source]
        error: Box<ParseError>,
    },

    #[error("While trying to make sense of your code...")]
    Type {
        path: PathBuf,
        src: String,
        named: NamedSource,
        #[source]
        error: tipo::error::Error,
    },

    #[error("{name} failed{}", if *verbose { format!("\n{src}") } else { String::new() } )]
    TestFailure {
        name: String,
        path: PathBuf,
        verbose: bool,
        src: String,
    },

    #[error(
        "I was unable to resolve '{}' for {}/{}",
        package.version,
        package.name.owner,
        package.name.repo
    )]
    UnknownPackageVersion { package: Package },

    #[error(
        "I need to resolve a package {}/{}, but couldn't find it.",
        package.name.owner,
        package.name.repo,
    )]
    UnableToResolvePackage { package: Package },

    #[error("I couldn't parse the provided stake address.")]
    MalformedStakeAddress {
        error: Option<pallas::ledger::addresses::Error>,
    },

    #[error("I didn't find any validator matching your criteria.")]
    NoValidatorNotFound { known_validators: Vec<String> },

    #[error("I found multiple suitable validators and I need you to tell me which one to pick.")]
    MoreThanOneValidatorFound { known_validators: Vec<String> },
}

impl Error {
    pub fn report(&self) {
        if let Error::TestFailure { verbose, .. } = self {
            if !verbose {
                return;
            }
        }

        println!("{self:?}")
    }

    pub fn from_parse_errors(errs: Vec<ParseError>, path: &Path, src: &str) -> Vec<Self> {
        let mut errors = Vec::with_capacity(errs.len());

        for error in errs {
            errors.push(Error::Parse {
                path: path.into(),
                src: src.to_string(),
                named: NamedSource::new(path.display().to_string(), src.to_string()),
                error: error.into(),
            });
        }

        errors
    }
}

impl Debug for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let miette_handler = MietteHandlerOpts::new()
            // For better support of terminal themes use the ANSI coloring
            .rgb_colors(RgbColors::Never)
            // If ansi support is disabled in the config disable the eye-candy
            .color(true)
            .unicode(true)
            .terminal_links(true)
            .build();

        // Ignore error to prevent format! panics. This can happen if span points at some
        // inaccessible location, for example by calling `report_error()` with wrong working set.
        let _ = miette_handler.debug(self, f);

        Ok(())
    }
}

impl From<Error> for Vec<Error> {
    fn from(value: Error) -> Self {
        vec![value]
    }
}

impl ExtraData for Error {
    fn extra_data(&self) -> Option<String> {
        match self {
            Error::DuplicateModule { .. } => None,
            Error::FileIo { .. } => None,
            Error::Format { .. } => None,
            Error::StandardIo { .. } => None,
            Error::Blueprint { .. } => None,
            Error::MissingManifest { .. } => None,
            Error::TomlLoading { .. } => None,
            Error::ImportCycle { .. } => None,
            Error::Parse { .. } => None,
            Error::Type { error, .. } => error.extra_data(),
            Error::TestFailure { .. } => None,
            Error::Http { .. } => None,
            Error::ZipExtract { .. } => None,
            Error::JoinError { .. } => None,
            Error::UnknownPackageVersion { .. } => None,
            Error::UnableToResolvePackage { .. } => None,
            Error::Json { .. } => None,
            Error::MalformedStakeAddress { .. } => None,
            Error::NoValidatorNotFound { .. } => None,
            Error::MoreThanOneValidatorFound { .. } => None,
            Error::Module { .. } => None,
        }
    }
}

pub trait GetSource {
    fn path(&self) -> Option<PathBuf>;
    fn src(&self) -> Option<String>;
}

impl GetSource for Error {
    fn path(&self) -> Option<PathBuf> {
        match self {
            Error::DuplicateModule { second, .. } => Some(second.to_path_buf()),
            Error::FileIo { .. } => None,
            Error::Format { .. } => None,
            Error::StandardIo(_) => None,
            Error::Blueprint(_) => None,
            Error::MissingManifest { path } => Some(path.to_path_buf()),
            Error::TomlLoading { path, .. } => Some(path.to_path_buf()),
            Error::ImportCycle { .. } => None,
            Error::Parse { path, .. } => Some(path.to_path_buf()),
            Error::Type { path, .. } => Some(path.to_path_buf()),
            Error::TestFailure { path, .. } => Some(path.to_path_buf()),
            Error::Http(_) => None,
            Error::ZipExtract(_) => None,
            Error::JoinError(_) => None,
            Error::UnknownPackageVersion { .. } => None,
            Error::UnableToResolvePackage { .. } => None,
            Error::Json { .. } => None,
            Error::MalformedStakeAddress { .. } => None,
            Error::NoValidatorNotFound { .. } => None,
            Error::MoreThanOneValidatorFound { .. } => None,
            Error::Module { .. } => None,
        }
    }

    fn src(&self) -> Option<String> {
        match self {
            Error::DuplicateModule { .. } => None,
            Error::FileIo { .. } => None,
            Error::Format { .. } => None,
            Error::StandardIo(_) => None,
            Error::Blueprint(_) => None,
            Error::MissingManifest { .. } => None,
            Error::TomlLoading { src, .. } => Some(src.to_string()),
            Error::ImportCycle { .. } => None,
            Error::Parse { src, .. } => Some(src.to_string()),
            Error::Type { src, .. } => Some(src.to_string()),
            Error::TestFailure { .. } => None,
            Error::Http(_) => None,
            Error::ZipExtract(_) => None,
            Error::JoinError(_) => None,
            Error::UnknownPackageVersion { .. } => None,
            Error::UnableToResolvePackage { .. } => None,
            Error::Json { .. } => None,
            Error::MalformedStakeAddress { .. } => None,
            Error::NoValidatorNotFound { .. } => None,
            Error::MoreThanOneValidatorFound { .. } => None,
            Error::Module { .. } => None,
        }
    }
}

impl Diagnostic for Error {
    fn severity(&self) -> Option<miette::Severity> {
        Some(miette::Severity::Error)
    }

    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        fn boxed<'a>(s: Box<dyn Display + 'a>) -> Box<dyn Display + 'a> {
            Box::new(format!(
                "        {} {}",
                "Error"
                    .if_supports_color(Stdout, |s| s.red())
                    .if_supports_color(Stdout, |s| s.bold()),
                format!("{s}").if_supports_color(Stdout, |s| s.red())
            ))
        }

        match self {
            Error::DuplicateModule { .. } => Some(boxed(Box::new("aiken::module::duplicate"))),
            Error::FileIo { .. } => None,
            Error::Blueprint(e) => e.code().map(boxed),
            Error::ImportCycle { .. } => Some(boxed(Box::new("aiken::module::cyclical"))),
            Error::Parse { .. } => Some(boxed(Box::new("aiken::parser"))),
            Error::Type { error, .. } => Some(boxed(Box::new(format!(
                "aiken::check{}",
                error.code().map(|s| format!("::{s}")).unwrap_or_default()
            )))),
            Error::StandardIo(_) => None,
            Error::MissingManifest { .. } => None,
            Error::TomlLoading { .. } => Some(boxed(Box::new("aiken::loading::toml"))),
            Error::Format { .. } => None,
            Error::TestFailure { path, .. } => Some(boxed(Box::new(path.to_str().unwrap_or("")))),
            Error::Http(_) => Some(Box::new("aiken::packages::download")),
            Error::ZipExtract(_) => None,
            Error::JoinError(_) => None,
            Error::UnknownPackageVersion { .. } => {
                Some(boxed(Box::new("aiken::packages::resolve")))
            }
            Error::UnableToResolvePackage { .. } => {
                Some(boxed(Box::new("aiken::package::download")))
            }
            Error::Json { .. } => None,
            Error::MalformedStakeAddress { .. } => None,
            Error::NoValidatorNotFound { .. } => None,
            Error::MoreThanOneValidatorFound { .. } => None,
            Error::Module(e) => e.code().map(boxed),
        }
    }

    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        match self {
            Error::DuplicateModule { first, second, .. } => Some(Box::new(format!(
                "Rename either {} or {}",
                first.display(),
                second.display()
            ))),
            Error::FileIo { error, .. } => Some(Box::new(format!("{error}"))),
            Error::Blueprint(e) => e.help(),
            Error::ImportCycle { modules } => Some(Box::new(format!(
                "Try moving the shared code to a separate module that the others can depend on\n- {}",
                modules.join("\n- ")
            ))),
            Error::Parse { error, .. } => error.kind.help(),
            Error::Type { error, .. } => error.help(),
            Error::StandardIo(_) => None,
            Error::MissingManifest { .. } => Some(Box::new(
                "Try running `aiken new <REPOSITORY/PROJECT>` to initialise a project with an example manifest.",
            )),
            Error::TomlLoading { .. } => None,
            Error::Format { .. } => None,
            Error::TestFailure { .. } => None,
            Error::Http(_) => None,
            Error::ZipExtract(_) => None,
            Error::JoinError(_) => None,
            Error::UnknownPackageVersion { .. } => Some(Box::new(
                "Perhaps, double-check the package repository and version?",
            )),
            Error::UnableToResolvePackage { .. } => Some(Box::new(
                "The network is unavailable and the package isn't in the local cache either. Try connecting to the Internet so I can look it up?",
            )),
            Error::Json(error) => Some(Box::new(format!("{error}"))),
            Error::MalformedStakeAddress { error } => Some(Box::new(format!(
                "A stake address must be provided either as a base16-encoded string, or as a bech32-encoded string with the 'stake' or 'stake_test' prefix.{hint}",
                hint = match error {
                    Some(error) => format!("\n\nHere's the error I encountered: {error}"),
                    None => String::new(),
                }
            ))),
            Error::NoValidatorNotFound { known_validators } => Some(Box::new(format!(
                "Here's a list of all validators I've found in your project. Please double-check this list against the options that you've provided:\n\n{}",
                known_validators
                    .iter()
                    .map(|title| format!(
                        "→ {title}",
                        title = title.if_supports_color(Stdout, |s| s.purple())
                    ))
                    .collect::<Vec<String>>()
                    .join("\n")
            ))),
            Error::MoreThanOneValidatorFound { known_validators } => Some(Box::new(format!(
                "Here's a list of all validators I've found in your project. Select one of them using the appropriate options:\n\n{}",
                known_validators
                    .iter()
                    .map(|title| format!(
                        "→ {title}",
                        title = title.if_supports_color(Stdout, |s| s.purple())
                    ))
                    .collect::<Vec<String>>()
                    .join("\n")
            ))),
            Error::Module(e) => e.help(),
        }
    }

    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
        match self {
            Error::DuplicateModule { .. } => None,
            Error::FileIo { .. } => None,
            Error::ImportCycle { .. } => None,
            Error::Blueprint(e) => e.labels(),
            Error::Parse { error, .. } => error.labels(),
            Error::MissingManifest { .. } => None,
            Error::Type { error, .. } => error.labels(),
            Error::StandardIo(_) => None,
            Error::TomlLoading { location, .. } => {
                if let Some(location) = location {
                    Some(Box::new(
                        vec![LabeledSpan::new_with_span(None, *location)].into_iter(),
                    ))
                } else {
                    None
                }
            }
            Error::Format { .. } => None,
            Error::TestFailure { .. } => None,
            Error::Http(_) => None,
            Error::ZipExtract(_) => None,
            Error::JoinError(_) => None,
            Error::UnknownPackageVersion { .. } => None,
            Error::UnableToResolvePackage { .. } => None,
            Error::Json { .. } => None,
            Error::MalformedStakeAddress { .. } => None,
            Error::NoValidatorNotFound { .. } => None,
            Error::MoreThanOneValidatorFound { .. } => None,
            Error::Module(e) => e.labels(),
        }
    }

    fn source_code(&self) -> Option<&dyn SourceCode> {
        match self {
            Error::DuplicateModule { .. } => None,
            Error::FileIo { .. } => None,
            Error::ImportCycle { .. } => None,
            Error::Blueprint(e) => e.source_code(),
            Error::Parse { named, .. } => Some(named),
            Error::Type { named, .. } => Some(named),
            Error::StandardIo(_) => None,
            Error::MissingManifest { .. } => None,
            Error::TomlLoading { named, .. } => Some(named.deref()),
            Error::Format { .. } => None,
            Error::TestFailure { .. } => None,
            Error::Http(_) => None,
            Error::ZipExtract(_) => None,
            Error::JoinError(_) => None,
            Error::UnknownPackageVersion { .. } => None,
            Error::UnableToResolvePackage { .. } => None,
            Error::Json { .. } => None,
            Error::MalformedStakeAddress { .. } => None,
            Error::NoValidatorNotFound { .. } => None,
            Error::MoreThanOneValidatorFound { .. } => None,
            Error::Module(e) => e.source_code(),
        }
    }

    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        match self {
            Error::DuplicateModule { .. } => None,
            Error::FileIo { .. } => None,
            Error::ImportCycle { .. } => None,
            Error::Blueprint(e) => e.url(),
            Error::Parse { .. } => None,
            Error::Type { error, .. } => error.url(),
            Error::StandardIo(_) => None,
            Error::MissingManifest { .. } => None,
            Error::TomlLoading { .. } => None,
            Error::Format { .. } => None,
            Error::TestFailure { .. } => None,
            Error::Http { .. } => None,
            Error::ZipExtract { .. } => None,
            Error::JoinError { .. } => None,
            Error::UnknownPackageVersion { .. } => None,
            Error::UnableToResolvePackage { .. } => None,
            Error::Json { .. } => None,
            Error::MalformedStakeAddress { .. } => None,
            Error::NoValidatorNotFound { .. } => None,
            Error::MoreThanOneValidatorFound { .. } => None,
            Error::Module(e) => e.url(),
        }
    }

    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
        match self {
            Error::DuplicateModule { .. } => None,
            Error::FileIo { .. } => None,
            Error::Blueprint(e) => e.related(),
            Error::ImportCycle { .. } => None,
            Error::Parse { .. } => None,
            Error::Type { error, .. } => error.related(),
            Error::StandardIo(_) => None,
            Error::MissingManifest { .. } => None,
            Error::TomlLoading { .. } => None,
            Error::Format { .. } => None,
            Error::TestFailure { .. } => None,
            Error::Http { .. } => None,
            Error::ZipExtract { .. } => None,
            Error::JoinError { .. } => None,
            Error::UnknownPackageVersion { .. } => None,
            Error::UnableToResolvePackage { .. } => None,
            Error::Json { .. } => None,
            Error::MalformedStakeAddress { .. } => None,
            Error::NoValidatorNotFound { .. } => None,
            Error::MoreThanOneValidatorFound { .. } => None,
            Error::Module(e) => e.related(),
        }
    }
}

#[derive(thiserror::Error)]
pub enum Warning {
    #[error("You do not have any validators to build!")]
    NoValidators,
    #[error("While trying to make sense of your code...")]
    Type {
        path: PathBuf,
        src: String,
        named: NamedSource,
        #[source]
        warning: tipo::error::Warning,
    },
    #[error("{name} is already a dependency.")]
    DependencyAlreadyExists { name: PackageName },
}

impl ExtraData for Warning {
    fn extra_data(&self) -> Option<String> {
        match self {
            Warning::NoValidators { .. } => None,
            Warning::DependencyAlreadyExists { .. } => None,
            Warning::Type { warning, .. } => warning.extra_data(),
        }
    }
}

impl GetSource for Warning {
    fn path(&self) -> Option<PathBuf> {
        match self {
            Warning::NoValidators => None,
            Warning::Type { path, .. } => Some(path.clone()),
            Warning::DependencyAlreadyExists { .. } => None,
        }
    }

    fn src(&self) -> Option<String> {
        match self {
            Warning::NoValidators => None,
            Warning::Type { src, .. } => Some(src.clone()),
            Warning::DependencyAlreadyExists { .. } => None,
        }
    }
}

impl Diagnostic for Warning {
    fn severity(&self) -> Option<miette::Severity> {
        Some(miette::Severity::Warning)
    }

    fn source_code(&self) -> Option<&dyn SourceCode> {
        match self {
            Warning::Type { named, .. } => Some(named),
            Warning::NoValidators => None,
            Warning::DependencyAlreadyExists { .. } => None,
        }
    }

    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
        match self {
            Warning::Type { warning, .. } => warning.labels(),
            Warning::NoValidators => None,
            Warning::DependencyAlreadyExists { .. } => None,
        }
    }

    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        fn boxed<'a>(s: Box<dyn Display + 'a>) -> Box<dyn Display + 'a> {
            Box::new(format!(
                "      {} {}",
                "Warning"
                    .if_supports_color(Stdout, |s| s.yellow())
                    .if_supports_color(Stdout, |s| s.bold()),
                format!("{s}").if_supports_color(Stdout, |s| s.yellow())
            ))
        }

        match self {
            Warning::Type { warning, .. } => Some(boxed(Box::new(format!(
                "aiken::check{}",
                warning.code().map(|s| format!("::{s}")).unwrap_or_default()
            )))),
            Warning::NoValidators => Some(boxed(Box::new("aiken::check"))),
            Warning::DependencyAlreadyExists { .. } => {
                Some(boxed(Box::new("aiken::packages::already_exists")))
            }
        }
    }

    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        match self {
            Warning::Type { warning, .. } => warning.help(),
            Warning::NoValidators => None,
            Warning::DependencyAlreadyExists { .. } => Some(Box::new(
                "If you need to change the version, try 'aiken packages upgrade' instead.",
            )),
        }
    }
}

impl Warning {
    pub fn from_type_warning(warning: tipo::error::Warning, path: PathBuf, src: String) -> Warning {
        Warning::Type {
            path: path.clone(),
            warning,
            src: src.clone(),
            named: NamedSource::new(path.display().to_string(), src),
        }
    }

    pub fn report(&self) {
        eprintln!("{self:?}")
    }
}

impl Debug for Warning {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let miette_handler = MietteHandlerOpts::new()
            // For better support of terminal themes use the ANSI coloring
            .rgb_colors(RgbColors::Never)
            // If ansi support is disabled in the config disable the eye-candy
            .color(true)
            .unicode(true)
            .terminal_links(true)
            .build();

        // Ignore error to prevent format! panics. This can happen if span points at some
        // inaccessible location, for example by calling `report_error()` with wrong working set.
        let _ = miette_handler.debug(self, f);

        Ok(())
    }
}

#[derive(Debug, PartialEq, Eq)]
pub struct Unformatted {
    pub source: PathBuf,
    pub destination: PathBuf,
    pub input: String,
    pub output: String,
}
