use std::cmp::Ordering;
use std::fmt::{self, Debug};
use std::fs;
use std::io;
use std::path::PathBuf;

use crate::colors::*;

use crate::cli::create_clickable_link;
use crate::filesystem::{file_to_module_path, walk_pyfiles, FileSystemError};
use crate::imports::{get_project_imports, ImportParseError, ProjectImport};

struct Dependency {
    file_path: PathBuf,
    absolute_path: PathBuf,
    import: ProjectImport,
}

#[derive(Debug)]
pub struct ReportCreationError {
    pub message: String,
}

impl fmt::Display for ReportCreationError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", &self.message)
    }
}

impl From<ImportParseError> for ReportCreationError {
    fn from(value: ImportParseError) -> Self {
        ReportCreationError {
            message: value.message,
        }
    }
}

impl From<FileSystemError> for ReportCreationError {
    fn from(value: FileSystemError) -> Self {
        ReportCreationError {
            message: value.message,
        }
    }
}

impl From<io::Error> for ReportCreationError {
    fn from(_: io::Error) -> Self {
        ReportCreationError {
            message: "I/O failure during report generation.".to_string(),
        }
    }
}

pub type Result<T> = std::result::Result<T, ReportCreationError>;

// less code than implementing/deriving all necessary traits for Ord
fn compare_dependencies(left: &Dependency, right: &Dependency) -> Ordering {
    let path_cmp = left.file_path.cmp(&right.file_path);
    if path_cmp == Ordering::Equal {
        return left.import.line_no.cmp(&right.import.line_no);
    }
    path_cmp
}

struct DependencyReport {
    path: String,
    pub external_dependencies: Vec<Dependency>,
    pub external_usages: Vec<Dependency>,
    pub warnings: Vec<String>,
}

impl DependencyReport {
    fn new(path: String) -> Self {
        DependencyReport {
            path,
            external_dependencies: vec![],
            external_usages: vec![],
            warnings: vec![],
        }
    }

    fn render_dependency(&self, dependency: &Dependency) -> String {
        let clickable_link = create_clickable_link(
            &dependency.file_path,
            &dependency.absolute_path,
            &dependency.import.line_no,
        );
        format!(
            "{green}{clickable_link}{end_color}: Import '{import_mod_path}'",
            green = BColors::OKGREEN,
            clickable_link = clickable_link,
            end_color = BColors::ENDC,
            import_mod_path = dependency.import.mod_path
        )
    }

    fn render_to_string(&mut self, skip_dependencies: bool, skip_usages: bool) -> String {
        let title = format!("Dependency Report for '{path}'", path = self.path.as_str());
        let mut result = format!(
            "[ {title} ]\n\
            -------------------------------\n",
            title = title,
        );

        if !skip_dependencies {
            let external_deps_title =
                format!("Dependencies of '{path}'", path = self.path.as_str());
            self.external_dependencies.sort_by(compare_dependencies);
            let deps_display: String = match self.external_dependencies.len() {
                0 => "No dependencies found.".to_string(),
                _ => self
                    .external_dependencies
                    .iter()
                    .map(|dep| self.render_dependency(dep))
                    .collect::<Vec<String>>()
                    .join("\n")
                    .to_string(),
            };
            result.push_str(&format!(
                "[ {deps_title} ]\n\
                {cyan}{deps}{end_color}\n\
                -------------------------------\n",
                deps_title = external_deps_title,
                deps = deps_display,
                cyan = BColors::OKCYAN,
                end_color = BColors::ENDC,
            ));
        }

        if !skip_usages {
            let external_usages_title = format!("Usages of '{path}'", path = self.path.as_str());
            self.external_usages.sort_by(compare_dependencies);
            let usages_display: String = match self.external_usages.len() {
                0 => "No usages found.".to_string(),
                _ => self
                    .external_usages
                    .iter()
                    .map(|dep| self.render_dependency(dep))
                    .collect::<Vec<String>>()
                    .join("\n")
                    .to_string(),
            };
            result.push_str(&format!(
                "[ {usages_title} ]\n\
                {cyan}{usages}{end_color}\n\
                -------------------------------\n",
                usages_title = external_usages_title,
                usages = usages_display,
                cyan = BColors::OKCYAN,
                end_color = BColors::ENDC,
            ));
        }

        if !self.warnings.is_empty() {
            result.push_str(&format!(
                "[ Warnings ]\n\
                {warning_color}{warnings}{end_color}",
                warning_color = BColors::WARNING,
                end_color = BColors::ENDC,
                warnings = self.warnings.join("\n")
            ));
        }

        result
    }
}

pub fn create_dependency_report(
    project_root: &PathBuf,
    source_roots: &[PathBuf],
    path: &PathBuf,
    include_dependency_modules: Option<Vec<String>>,
    include_usage_modules: Option<Vec<String>>,
    skip_dependencies: bool,
    skip_usages: bool,
    ignore_type_checking_imports: bool,
) -> Result<String> {
    if skip_dependencies && skip_usages {
        return Err(ReportCreationError {
            message: "Nothing to report when skipping dependencies and usages.".to_string(),
        });
    }
    let source_roots: Vec<PathBuf> = source_roots.iter().map(PathBuf::from).collect();
    let absolute_path = PathBuf::from(&project_root).join(fs::canonicalize(path)?);
    let module_path = file_to_module_path(&source_roots, &absolute_path)?;
    let mut result = DependencyReport::new(path.to_string_lossy().to_string()); // TODO: clone shouldnt be necessary

    for pyfile in walk_pyfiles(project_root.to_str().unwrap()) {
        let absolute_pyfile = PathBuf::from(&project_root).join(&pyfile);
        match get_project_imports(
            project_root,
            &source_roots,
            &absolute_pyfile,
            ignore_type_checking_imports,
        ) {
            Ok(project_imports) => {
                let pyfile_in_target_module = absolute_pyfile.starts_with(&absolute_path);
                if pyfile_in_target_module && !skip_dependencies {
                    // Any import from within the target module which points to an external mod_path
                    // is an external dependency
                    result.external_dependencies.extend(
                        project_imports
                            .into_iter()
                            .filter(|import| {
                                if import.mod_path.starts_with(&module_path) {
                                    // this is an internal import
                                    return false;
                                }

                                // for external imports,
                                // if there is a filter list of dependencies, verify that the import is included
                                match include_dependency_modules {
                                    None => true,
                                    Some(ref included_modules) => {
                                        for module_path in included_modules {
                                            if import.mod_path.starts_with(module_path) {
                                                return true;
                                            }
                                        }
                                        false
                                    }
                                }
                            })
                            .map(|import| Dependency {
                                file_path: pyfile.clone(),
                                absolute_path: absolute_pyfile.clone(),
                                import,
                            }),
                    );
                } else if !skip_usages {
                    // We are looking at imports from outside the target module,
                    // so any import which points to the target module is an external usage
                    for import in project_imports {
                        if !import.mod_path.starts_with(&module_path) {
                            // this import doesn't point to the target module
                            continue;
                        }

                        let pyfile_mod_path = file_to_module_path(&source_roots, &absolute_pyfile);
                        if pyfile_mod_path.is_err() {
                            // the current file doesn't belong to the source root
                            continue;
                        }

                        if include_usage_modules.is_none()
                            || include_usage_modules
                                .as_ref()
                                .is_some_and(|included_modules| {
                                    included_modules.contains(&pyfile_mod_path.unwrap())
                                })
                        {
                            result.external_usages.push(Dependency {
                                file_path: pyfile.clone(),
                                absolute_path: absolute_pyfile.clone(),
                                import,
                            });
                        }
                    }
                }
            }
            Err(err) => {
                // Failed to parse project imports
                result.warnings.push(err.message);
            }
        }
    }
    Ok(result.render_to_string(skip_dependencies, skip_usages))
}
