use crate::config::CheckerConfig;
use crate::parser::{ParserAdapter, RustPythonParser};
use crate::report::{FileReport, Report, Summary};
use crate::rules::run_rules;
use globset::{Glob, GlobSet, GlobSetBuilder};
use std::fs;
use std::path::{Path, PathBuf};
use thiserror::Error;
use walkdir::WalkDir;

#[derive(Debug, Error)]
pub enum CheckerError {
    #[error("failed to build glob matcher: {0}")]
    InvalidPattern(String),
    #[error("failed to access path: {0}")]
    Io(#[from] std::io::Error),
}

#[must_use]
pub fn check_code(source: &str, virtual_path: &str, config: &CheckerConfig) -> Report {
    let parser = RustPythonParser;
    let file_report = analyze_source(&parser, source, virtual_path, config);
    build_report(vec![file_report])
}

/// 指定したファイルまたはディレクトリを検査します。
///
/// # Errors
///
/// パス走査、ファイル読み込み、または glob 構築に失敗した場合に返します。
pub fn check_path(path: impl AsRef<Path>, config: &CheckerConfig) -> Result<Report, CheckerError> {
    let path = path.as_ref();
    let matcher = PathMatcher::new(config, path)?;
    let parser = RustPythonParser;
    let mut file_reports = Vec::new();

    if path.is_file() {
        if matcher.should_check(path) {
            let source = fs::read_to_string(path)?;
            file_reports.push(analyze_source(
                &parser,
                &source,
                &normalize_path(path),
                config,
            ));
        }
        return Ok(build_report(file_reports));
    }

    for entry in WalkDir::new(path) {
        let entry = match entry {
            Ok(entry) => entry,
            Err(error) => {
                return Err(CheckerError::Io(std::io::Error::new(
                    std::io::ErrorKind::Other,
                    error.to_string(),
                )))
            }
        };
        if entry.file_type().is_file() && matcher.should_check(entry.path()) {
            let source = fs::read_to_string(entry.path())?;
            file_reports.push(analyze_source(
                &parser,
                &source,
                &normalize_path(entry.path()),
                config,
            ));
        }
    }

    Ok(build_report(file_reports))
}

fn analyze_source(
    parser: &impl ParserAdapter,
    source: &str,
    path: &str,
    config: &CheckerConfig,
) -> FileReport {
    let parsed = parser.parse_module(source, path, &config.ignore_decorator_name);
    let checked_functions = parsed
        .module
        .functions
        .iter()
        .filter(|function| !function.is_ignored())
        .count();
    let ignored_functions = parsed
        .module
        .functions
        .iter()
        .filter(|function| function.is_ignored())
        .count();
    let mut diagnostics = parsed.diagnostics;

    for function in &parsed.module.functions {
        if function.is_ignored() {
            continue;
        }
        diagnostics.extend(run_rules(&parsed.module, function, config));
    }

    diagnostics.sort_by(|left, right| {
        (
            left.path.as_str(),
            left.location.start.line,
            left.location.start.column,
            left.rule_id.as_str(),
        )
            .cmp(&(
                right.path.as_str(),
                right.location.start.line,
                right.location.start.column,
                right.rule_id.as_str(),
            ))
    });

    FileReport {
        path: path.to_owned(),
        checked_functions,
        ignored_functions,
        diagnostics,
    }
}

fn build_report(mut files: Vec<FileReport>) -> Report {
    files.sort_by(|left, right| left.path.cmp(&right.path));
    let summary = Summary {
        files: files.len(),
        checked_functions: files.iter().map(|file| file.checked_functions).sum(),
        ignored_functions: files.iter().map(|file| file.ignored_functions).sum(),
        diagnostics: files.iter().map(|file| file.diagnostics.len()).sum(),
    };
    Report { summary, files }
}

struct PathMatcher {
    include: Option<GlobSet>,
    exclude: GlobSet,
    match_root: PathBuf,
}

impl PathMatcher {
    fn new(config: &CheckerConfig, target: &Path) -> Result<Self, CheckerError> {
        let include = if config.include.is_empty() {
            None
        } else {
            Some(build_globset(&config.include)?)
        };
        let exclude = build_globset(&config.exclude)?;
        let match_root = config.base_dir.clone().unwrap_or_else(|| {
            if target.is_dir() {
                target.to_path_buf()
            } else {
                target
                    .parent()
                    .map_or_else(|| PathBuf::from("."), Path::to_path_buf)
            }
        });
        Ok(Self {
            include,
            exclude,
            match_root,
        })
    }

    fn should_check(&self, path: &Path) -> bool {
        let absolute_path = normalize_path(path);
        let relative_path = path.strip_prefix(&self.match_root).ok().map(normalize_path);
        Path::new(&absolute_path)
            .extension()
            .is_some_and(|extension| extension.eq_ignore_ascii_case("py"))
            && !matches_globset(&self.exclude, &absolute_path, relative_path.as_deref())
            && self.include.as_ref().map_or(true, |include| {
                matches_globset(include, &absolute_path, relative_path.as_deref())
            })
    }
}

fn matches_globset(globset: &GlobSet, absolute_path: &str, relative_path: Option<&str>) -> bool {
    globset.is_match(absolute_path) || relative_path.is_some_and(|path| globset.is_match(path))
}

fn build_globset(patterns: &[String]) -> Result<GlobSet, CheckerError> {
    let mut builder = GlobSetBuilder::new();
    for pattern in patterns {
        let glob = Glob::new(pattern)
            .map_err(|error| CheckerError::InvalidPattern(format!("{pattern}: {error}")))?;
        builder.add(glob);
    }
    builder
        .build()
        .map_err(|error| CheckerError::InvalidPattern(error.to_string()))
}

fn normalize_path(path: &Path) -> String {
    path.to_string_lossy().replace('\\', "/")
}
