pub mod cache;
pub mod checks;
pub mod cli;
pub mod colors;
pub mod commands;
pub mod config;
pub mod dependencies;
pub mod diagnostics;
pub mod exclusion;
pub mod external;
pub mod filesystem;
pub mod interfaces;
pub mod interrupt;
pub mod lsp;
pub mod modularity;
pub mod modules;
pub mod parsing;
pub mod pattern;
pub mod processors;
pub mod python;
pub mod tests;

use commands::{check, report, server, sync, test};
use diagnostics::serialize_diagnostics_json;
use modularity::into_usage_errors;
use std::collections::HashMap;
use std::path::PathBuf;

use pyo3::exceptions::{PyKeyboardInterrupt, PyOSError, PySyntaxError, PyValueError};
use pyo3::prelude::*;

mod errors {
    pyo3::import_exception!(tach.errors, TachCircularDependencyError);
    pyo3::import_exception!(tach.errors, TachVisibilityError);
    pyo3::import_exception!(tach.errors, TachSetupError);
}

impl From<processors::import::ImportParseError> for PyErr {
    fn from(err: processors::import::ImportParseError) -> Self {
        match err {
            processors::import::ImportParseError::Parsing { file: _, source: _ } => {
                PySyntaxError::new_err(err.to_string())
            }
            _ => PyOSError::new_err(err.to_string()),
        }
    }
}

impl From<exclusion::PathExclusionError> for PyErr {
    fn from(err: exclusion::PathExclusionError) -> Self {
        match err {
            exclusion::PathExclusionError::ConcurrencyError => PyOSError::new_err(err.to_string()),
            _ => PyValueError::new_err(err.to_string()),
        }
    }
}

impl From<report::ReportCreationError> for PyErr {
    fn from(err: report::ReportCreationError) -> Self {
        PyValueError::new_err(err.to_string())
    }
}

impl From<cache::CacheError> for PyErr {
    fn from(err: cache::CacheError) -> Self {
        PyValueError::new_err(err.to_string())
    }
}

impl From<check::CheckError> for PyErr {
    fn from(err: check::CheckError) -> Self {
        match err {
            check::CheckError::Interrupt => PyKeyboardInterrupt::new_err(err.to_string()),
            check::CheckError::ModuleTree(modules::error::ModuleTreeError::CircularDependency(
                c,
            )) => errors::TachCircularDependencyError::new_err(c),
            check::CheckError::ModuleTree(
                modules::error::ModuleTreeError::VisibilityViolation(v),
            ) => errors::TachVisibilityError::new_err(v),
            _ => PyValueError::new_err(err.to_string()),
        }
    }
}

impl From<python::error::ParsingError> for PyErr {
    fn from(err: python::error::ParsingError) -> Self {
        match err {
            python::error::ParsingError::PythonParse(err) => {
                PySyntaxError::new_err(err.to_string())
            }
            python::error::ParsingError::Io(err) => PyOSError::new_err(err.to_string()),
            python::error::ParsingError::Filesystem(err) => PyOSError::new_err(err.to_string()),
            python::error::ParsingError::InvalidSyntax => PySyntaxError::new_err(err.to_string()),
        }
    }
}

impl From<parsing::error::ParsingError> for PyErr {
    fn from(err: parsing::error::ParsingError) -> Self {
        match err {
            parsing::error::ParsingError::Io(err) => PyOSError::new_err(err.to_string()),
            parsing::error::ParsingError::Filesystem(err) => PyOSError::new_err(err.to_string()),
            parsing::error::ParsingError::TomlParse(err) => PyValueError::new_err(err.to_string()),
            parsing::error::ParsingError::MissingField(err) => PyValueError::new_err(err),
            parsing::error::ParsingError::ModulePath(err) => PyValueError::new_err(err),
        }
    }
}
impl From<sync::SyncError> for PyErr {
    fn from(err: sync::SyncError) -> Self {
        match err {
            sync::SyncError::FileWrite(err) => PyOSError::new_err(err.to_string()),
            sync::SyncError::TomlSerialize(err) => PyOSError::new_err(err.to_string()),
            sync::SyncError::CheckError(err) => err.into(),
            sync::SyncError::RootModuleViolation(err) => PyValueError::new_err(err.to_string()),
            sync::SyncError::EditError(err) => PyValueError::new_err(err.to_string()),
        }
    }
}

impl From<lsp::error::ServerError> for PyErr {
    fn from(err: lsp::error::ServerError) -> Self {
        match err {
            lsp::error::ServerError::Initialize => errors::TachSetupError::new_err(err.to_string()),
            _ => PyOSError::new_err(err.to_string()),
        }
    }
}

impl From<config::edit::EditError> for PyErr {
    fn from(err: config::edit::EditError) -> Self {
        PyValueError::new_err(err.to_string())
    }
}

impl From<config::error::ConfigError> for PyErr {
    fn from(err: config::error::ConfigError) -> Self {
        PyValueError::new_err(err.to_string())
    }
}

impl IntoPy<PyObject> for modules::error::VisibilityErrorInfo {
    fn into_py(self, py: pyo3::prelude::Python<'_>) -> PyObject {
        (
            self.dependent_module,
            self.dependency_module,
            self.visibility,
        )
            .into_py(py)
    }
}

/// Parse project config
#[pyfunction]
fn parse_project_config(
    filepath: PathBuf,
) -> parsing::config::Result<(config::ProjectConfig, bool)> {
    parsing::config::parse_project_config(filepath)
}

#[pyfunction]
#[pyo3(signature = (config))]
fn dump_project_config_to_toml(
    config: &mut config::ProjectConfig,
) -> Result<String, sync::SyncError> {
    // TODO: Error handling hack
    parsing::config::dump_project_config_to_toml(config).map_err(sync::SyncError::TomlSerialize)
}

/// Get first-party imports from file_path
#[pyfunction]
fn get_project_imports(
    project_root: PathBuf,
    source_roots: Vec<PathBuf>,
    file_path: PathBuf,
    project_config: config::ProjectConfig,
) -> processors::import::Result<Vec<dependencies::LocatedImport>> {
    commands::helpers::import::get_located_project_imports(
        &project_root,
        &source_roots,
        &file_path,
        &project_config,
    )
}

/// Get third-party imports from file_path
#[pyfunction]
fn get_external_imports(
    project_root: PathBuf,
    source_roots: Vec<PathBuf>,
    file_path: PathBuf,
    project_config: config::ProjectConfig,
) -> processors::import::Result<Vec<dependencies::LocatedImport>> {
    commands::helpers::import::get_located_external_imports(
        &project_root,
        &source_roots,
        &file_path,
        &project_config,
    )
}

/// Validate external dependency imports against pyproject.toml dependencies
#[pyfunction]
fn check_external_dependencies(
    project_root: PathBuf,
    project_config: config::ProjectConfig,
    module_mappings: HashMap<String, Vec<String>>,
    stdlib_modules: Vec<String>,
) -> check::check_external::Result<Vec<diagnostics::Diagnostic>> {
    check::check_external::check(
        &project_root,
        &project_config,
        &module_mappings,
        &stdlib_modules,
    )
}

/// Create a report of dependencies and usages of a given path
#[pyfunction]
#[pyo3(signature = (project_root, project_config, path, include_dependency_modules, include_usage_modules, skip_dependencies, skip_usages, raw))]
fn create_dependency_report(
    project_root: PathBuf,
    project_config: &config::ProjectConfig,
    path: PathBuf,
    include_dependency_modules: Option<Vec<String>>,
    include_usage_modules: Option<Vec<String>>,
    skip_dependencies: bool,
    skip_usages: bool,
    raw: bool,
) -> report::Result<String> {
    report::create_dependency_report(
        &project_root,
        project_config,
        &path,
        include_dependency_modules,
        include_usage_modules,
        skip_dependencies,
        skip_usages,
        raw,
    )
}

#[pyfunction]
fn create_computation_cache_key(
    project_root: PathBuf,
    source_roots: Vec<PathBuf>,
    action: String,
    py_interpreter_version: String,
    file_dependencies: Vec<String>,
    env_dependencies: Vec<String>,
    backend: String,
) -> String {
    cache::create_computation_cache_key(
        &project_root,
        &source_roots,
        action,
        py_interpreter_version,
        file_dependencies,
        env_dependencies,
        backend,
    )
}

#[pyfunction]
fn check_computation_cache(
    project_root: PathBuf,
    cache_key: String,
) -> cache::Result<Option<cache::ComputationCacheValue>> {
    cache::check_computation_cache(&project_root, cache_key)
}

#[pyfunction]
fn update_computation_cache(
    project_root: PathBuf,
    cache_key: String,
    value: cache::ComputationCacheValue,
) -> cache::Result<Option<cache::ComputationCacheValue>> {
    cache::update_computation_cache(&project_root, cache_key, value)
}

#[pyfunction]
#[pyo3(name = "check")]
fn check_internal(
    project_root: PathBuf,
    project_config: &config::ProjectConfig,
    dependencies: bool,
    interfaces: bool,
) -> check::check_internal::Result<Vec<diagnostics::Diagnostic>> {
    check::check_internal(project_root, project_config, dependencies, interfaces)
}

#[pyfunction]
pub fn format_diagnostics(
    project_root: PathBuf,
    diagnostics: Vec<diagnostics::Diagnostic>,
) -> String {
    check::format::DiagnosticFormatter::new(project_root).format_diagnostics(&diagnostics)
}

#[pyfunction]
fn detect_unused_dependencies(
    project_root: PathBuf,
    project_config: &mut config::ProjectConfig,
) -> Result<Vec<sync::UnusedDependencies>, sync::SyncError> {
    sync::detect_unused_dependencies(project_root, project_config)
}

#[pyfunction]
#[pyo3(signature = (project_root, project_config, add = false))]
pub fn sync_project(
    project_root: PathBuf,
    project_config: config::ProjectConfig,
    add: bool,
) -> Result<(), sync::SyncError> {
    sync::sync_project(project_root, project_config, add)
}

#[pyfunction]
fn run_server(
    project_root: PathBuf,
    project_config: config::ProjectConfig,
) -> Result<(), lsp::error::ServerError> {
    server::run_server(project_root, project_config)
}

#[pyfunction]
fn serialize_modules_json(modules: Vec<config::ModuleConfig>) -> String {
    config::serialize_modules_json(&modules)
}

#[pymodule]
fn extension(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
    interrupt::setup_interrupt_handler();
    m.add_class::<config::ProjectConfig>()?;
    m.add_class::<config::ModuleConfig>()?;
    m.add_class::<config::InterfaceConfig>()?;
    m.add_class::<config::RulesConfig>()?;
    m.add_class::<config::DependencyConfig>()?;
    m.add_class::<diagnostics::Diagnostic>()?;
    m.add_class::<test::TachPytestPluginHandler>()?;
    m.add_class::<modularity::UsageError>()?;
    m.add_function(wrap_pyfunction_bound!(parse_project_config, m)?)?;
    m.add_function(wrap_pyfunction_bound!(get_project_imports, m)?)?;
    m.add_function(wrap_pyfunction_bound!(get_external_imports, m)?)?;
    m.add_function(wrap_pyfunction_bound!(check_external_dependencies, m)?)?;
    m.add_function(wrap_pyfunction_bound!(create_dependency_report, m)?)?;
    m.add_function(wrap_pyfunction_bound!(create_computation_cache_key, m)?)?;
    m.add_function(wrap_pyfunction_bound!(check_computation_cache, m)?)?;
    m.add_function(wrap_pyfunction_bound!(update_computation_cache, m)?)?;
    m.add_function(wrap_pyfunction_bound!(dump_project_config_to_toml, m)?)?;
    m.add_function(wrap_pyfunction_bound!(check_internal, m)?)?;
    m.add_function(wrap_pyfunction_bound!(format_diagnostics, m)?)?;
    m.add_function(wrap_pyfunction_bound!(detect_unused_dependencies, m)?)?;
    m.add_function(wrap_pyfunction_bound!(sync_project, m)?)?;
    m.add_function(wrap_pyfunction_bound!(run_server, m)?)?;
    m.add_function(wrap_pyfunction_bound!(serialize_modules_json, m)?)?;
    m.add_function(wrap_pyfunction_bound!(serialize_diagnostics_json, m)?)?;
    m.add_function(wrap_pyfunction_bound!(into_usage_errors, m)?)?;
    Ok(())
}
