use crate::model::{ExternalPurityCategory, ImplementationStyleCategory, RuleId, Severity};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use thiserror::Error;

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum OutputFormat {
    #[default]
    Text,
    Json,
    Sarif,
}

impl FromStr for OutputFormat {
    type Err = String;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        match value {
            "text" => Ok(Self::Text),
            "json" => Ok(Self::Json),
            "sarif" => Ok(Self::Sarif),
            _ => Err(format!("unsupported output format: {value}")),
        }
    }
}

#[derive(Debug, Clone)]
pub struct CheckerConfig {
    pub format: OutputFormat,
    pub fail_on: Severity,
    pub include: Vec<String>,
    pub exclude: Vec<String>,
    pub enabled_rules: Option<BTreeSet<RuleId>>,
    pub ignore_decorator_name: String,
    pub base_dir: Option<PathBuf>,
    pub purity: PurityConfig,
    pub style: StyleConfig,
    pub allowlists: AllowlistsConfig,
    pub custom_rules: Vec<CustomRuleConfig>,
}

impl Default for CheckerConfig {
    fn default() -> Self {
        Self {
            format: OutputFormat::Text,
            fail_on: Severity::Warning,
            include: Vec::new(),
            exclude: vec![
                "**/.git/**".to_owned(),
                "**/.venv/**".to_owned(),
                "**/__pycache__/**".to_owned(),
                "**/.mypy_cache/**".to_owned(),
                "**/.pytest_cache/**".to_owned(),
            ],
            enabled_rules: None,
            ignore_decorator_name: "fp_ignore".to_owned(),
            base_dir: None,
            purity: PurityConfig::default(),
            style: StyleConfig::default(),
            allowlists: AllowlistsConfig::default(),
            custom_rules: Vec::new(),
        }
    }
}

impl CheckerConfig {
    #[must_use]
    pub fn is_rule_enabled(&self, rule_id: &RuleId) -> bool {
        self.enabled_rules
            .as_ref()
            .is_none_or(|enabled_rules| enabled_rules.contains(rule_id))
    }

    /// 設定の整合性を検証します。
    ///
    /// # Errors
    ///
    /// custom rule 定義や rule 選択に矛盾がある場合に返します。
    pub fn validate(&self) -> Result<(), ConfigError> {
        let builtins = RuleId::all().into_iter().collect::<BTreeSet<_>>();
        let mut custom_rule_ids = BTreeSet::new();

        for rule in &self.custom_rules {
            if rule.patterns.is_empty() {
                return Err(ConfigError::InvalidCustomRule(format!(
                    "custom rule `{}` must define at least one pattern",
                    rule.id
                )));
            }
            if rule.message.trim().is_empty() {
                return Err(ConfigError::InvalidCustomRule(format!(
                    "custom rule `{}` must define a non-empty message",
                    rule.id
                )));
            }
            if !matches!(rule.id, RuleId::Custom(_)) {
                return Err(ConfigError::InvalidCustomRule(format!(
                    "custom rule id `{}` conflicts with a built-in rule id",
                    rule.id
                )));
            }
            if !custom_rule_ids.insert(rule.id.clone()) {
                return Err(ConfigError::InvalidCustomRule(format!(
                    "custom rule id `{}` is duplicated",
                    rule.id
                )));
            }
        }

        if let Some(enabled_rules) = &self.enabled_rules {
            for rule_id in enabled_rules {
                if !builtins.contains(rule_id) && !custom_rule_ids.contains(rule_id) {
                    return Err(ConfigError::InvalidRuleSelection(rule_id.to_string()));
                }
            }
        }

        Ok(())
    }

    /// 部分設定を現在の設定へマージします。
    ///
    /// # Errors
    ///
    /// 不正な値は設定ファイル読み込み時に検出されるため、マージ時は既存設定を上書きするだけです。
    #[must_use]
    pub fn merge(&self, other: PartialCheckerConfig) -> Self {
        let enabled_rules = other
            .rules
            .map(|rules| rules.into_iter().collect())
            .or_else(|| self.enabled_rules.clone());

        Self {
            format: other.format.unwrap_or(self.format),
            fail_on: other.fail_on.unwrap_or(self.fail_on),
            include: other.include.unwrap_or_else(|| self.include.clone()),
            exclude: other.exclude.unwrap_or_else(|| self.exclude.clone()),
            enabled_rules,
            ignore_decorator_name: other
                .ignore_decorator_name
                .unwrap_or_else(|| self.ignore_decorator_name.clone()),
            base_dir: self.base_dir.clone(),
            purity: merge_purity_config(self.purity, other.purity),
            style: merge_style_config(self.style, other.style),
            allowlists: merge_allowlists_config(&self.allowlists, other.allowlists),
            custom_rules: other
                .custom_rules
                .unwrap_or_else(|| self.custom_rules.clone()),
        }
    }
}

#[derive(Debug, Error)]
pub enum ConfigError {
    #[error("failed to read config file: {0}")]
    Io(#[from] std::io::Error),
    #[error("failed to parse config file: {0}")]
    Parse(#[from] toml::de::Error),
    #[error("invalid custom rule: {0}")]
    InvalidCustomRule(String),
    #[error("unknown rule selection: {0}")]
    InvalidRuleSelection(String),
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CustomRuleTarget {
    Call,
    Name,
    AttributeWrite,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CustomRuleMatchType {
    #[default]
    Exact,
    Prefix,
    Suffix,
    Contains,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct CustomRuleConfig {
    pub id: RuleId,
    pub target: CustomRuleTarget,
    #[serde(default)]
    pub match_type: CustomRuleMatchType,
    pub patterns: Vec<String>,
    pub message: String,
    #[serde(default)]
    pub severity: Severity,
    #[serde(default)]
    pub hint: Option<String>,
    #[serde(default = "default_custom_rule_enabled")]
    pub enabled: bool,
}

fn default_custom_rule_enabled() -> bool {
    true
}

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum UnknownCallBehavior {
    #[default]
    AssumePure,
    AssumeImpure,
    WarnUnknown,
    RequireAnnotation,
}

impl fmt::Display for UnknownCallBehavior {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let value = match self {
            Self::AssumePure => "assume_pure",
            Self::AssumeImpure => "assume_impure",
            Self::WarnUnknown => "warn_unknown",
            Self::RequireAnnotation => "require_annotation",
        };
        f.write_str(value)
    }
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub struct PurityConfig {
    pub enable: bool,
    pub treat_unknown_calls_as: UnknownCallBehavior,
    pub self_attribute_reads_as_external_state: bool,
    pub treat_local_mutation_as_impurity: bool,
    pub severity: PuritySeverityConfig,
}

impl Default for PurityConfig {
    fn default() -> Self {
        Self {
            enable: true,
            treat_unknown_calls_as: UnknownCallBehavior::AssumePure,
            self_attribute_reads_as_external_state: true,
            treat_local_mutation_as_impurity: false,
            severity: PuritySeverityConfig::default(),
        }
    }
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub struct PuritySeverityConfig {
    pub pure: Severity,
    pub impure_reads_external_state: Severity,
    pub impure_writes_external_state: Severity,
    pub impure_direct_io: Severity,
    pub impure_nondeterministic: Severity,
    pub impure_unknown_call: Severity,
    pub impure_exceptional_control_dependency: Severity,
}

impl Default for PuritySeverityConfig {
    fn default() -> Self {
        Self {
            pure: Severity::Info,
            impure_reads_external_state: Severity::Warning,
            impure_writes_external_state: Severity::Error,
            impure_direct_io: Severity::Error,
            impure_nondeterministic: Severity::Error,
            impure_unknown_call: Severity::Warning,
            impure_exceptional_control_dependency: Severity::Warning,
        }
    }
}

impl PuritySeverityConfig {
    #[must_use]
    pub fn severity_for(&self, category: ExternalPurityCategory) -> Severity {
        match category {
            ExternalPurityCategory::Pure => self.pure,
            ExternalPurityCategory::ImpureReadsExternalState => self.impure_reads_external_state,
            ExternalPurityCategory::ImpureWritesExternalState => self.impure_writes_external_state,
            ExternalPurityCategory::ImpureDirectIo => self.impure_direct_io,
            ExternalPurityCategory::ImpureNondeterministic => self.impure_nondeterministic,
            ExternalPurityCategory::ImpureUnknownCall => self.impure_unknown_call,
            ExternalPurityCategory::ImpureExceptionalControlDependency => {
                self.impure_exceptional_control_dependency
            }
        }
    }
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub struct StyleConfig {
    pub enable: bool,
    pub reassignment_threshold: usize,
    pub mutation_threshold: usize,
    pub imperative_style_threshold: usize,
    pub severity: StyleSeverityConfig,
}

impl Default for StyleConfig {
    fn default() -> Self {
        Self {
            enable: true,
            reassignment_threshold: 3,
            mutation_threshold: 2,
            imperative_style_threshold: 8,
            severity: StyleSeverityConfig::default(),
        }
    }
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub struct StyleSeverityConfig {
    pub no_reassignment: Severity,
    pub has_reassignment: Severity,
    pub reassignment_heavy: Severity,
    pub has_mutation: Severity,
    pub mutation_heavy: Severity,
    pub imperative_style_high: Severity,
}

impl Default for StyleSeverityConfig {
    fn default() -> Self {
        Self {
            no_reassignment: Severity::Info,
            has_reassignment: Severity::Info,
            reassignment_heavy: Severity::Warning,
            has_mutation: Severity::Info,
            mutation_heavy: Severity::Warning,
            imperative_style_high: Severity::Error,
        }
    }
}

impl StyleSeverityConfig {
    #[must_use]
    pub fn severity_for(&self, category: ImplementationStyleCategory) -> Severity {
        match category {
            ImplementationStyleCategory::NoReassignment => self.no_reassignment,
            ImplementationStyleCategory::HasReassignment => self.has_reassignment,
            ImplementationStyleCategory::ReassignmentHeavy => self.reassignment_heavy,
            ImplementationStyleCategory::HasMutation => self.has_mutation,
            ImplementationStyleCategory::MutationHeavy => self.mutation_heavy,
            ImplementationStyleCategory::ImperativeStyleHigh => self.imperative_style_high,
        }
    }
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub struct AllowlistsConfig {
    #[serde(default)]
    pub pure_calls: Vec<String>,
    #[serde(default)]
    pub impure_calls: Vec<String>,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PartialCheckerConfig {
    pub format: Option<OutputFormat>,
    pub fail_on: Option<Severity>,
    pub include: Option<Vec<String>>,
    pub exclude: Option<Vec<String>>,
    pub rules: Option<Vec<RuleId>>,
    pub ignore_decorator_name: Option<String>,
    pub purity: Option<PartialPurityConfig>,
    pub style: Option<PartialStyleConfig>,
    pub allowlists: Option<AllowlistsConfig>,
    pub custom_rules: Option<Vec<CustomRuleConfig>>,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PartialPurityConfig {
    pub enable: Option<bool>,
    pub treat_unknown_calls_as: Option<UnknownCallBehavior>,
    pub self_attribute_reads_as_external_state: Option<bool>,
    pub treat_local_mutation_as_impurity: Option<bool>,
    pub severity: Option<PartialPuritySeverityConfig>,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PartialStyleConfig {
    pub enable: Option<bool>,
    pub reassignment_threshold: Option<usize>,
    pub mutation_threshold: Option<usize>,
    pub imperative_style_threshold: Option<usize>,
    pub severity: Option<PartialStyleSeverityConfig>,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PartialPuritySeverityConfig {
    pub pure: Option<Severity>,
    pub impure_reads_external_state: Option<Severity>,
    pub impure_writes_external_state: Option<Severity>,
    pub impure_direct_io: Option<Severity>,
    pub impure_nondeterministic: Option<Severity>,
    pub impure_unknown_call: Option<Severity>,
    pub impure_exceptional_control_dependency: Option<Severity>,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PartialStyleSeverityConfig {
    pub no_reassignment: Option<Severity>,
    pub has_reassignment: Option<Severity>,
    pub reassignment_heavy: Option<Severity>,
    pub has_mutation: Option<Severity>,
    pub mutation_heavy: Option<Severity>,
    pub imperative_style_high: Option<Severity>,
}

#[derive(Debug, Deserialize)]
struct PyProjectFile {
    tool: Option<PyProjectTool>,
}

#[derive(Debug, Deserialize)]
struct PyProjectTool {
    fp_checker: Option<PartialCheckerConfig>,
    #[serde(rename = "fp-checker")]
    fp_checker_dash: Option<PartialCheckerConfig>,
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct DedicatedConfigFile {
    fp_checker: Option<PartialCheckerConfig>,
    #[serde(rename = "fp-checker")]
    fp_checker_dash: Option<PartialCheckerConfig>,
}

/// 設定ファイルを読み込み、`CheckerConfig` へ変換します。
///
/// # Errors
///
/// ファイルの読み込みまたは TOML の解釈に失敗した場合に返します。
pub fn load_config_from_path(path: impl AsRef<Path>) -> Result<CheckerConfig, ConfigError> {
    let path = path.as_ref();
    let content = fs::read_to_string(path)?;
    let partial = if path.file_name().and_then(|name| name.to_str()) == Some("pyproject.toml") {
        let file: PyProjectFile = toml::from_str(&content)?;
        file.tool
            .and_then(|tool| tool.fp_checker.or(tool.fp_checker_dash))
            .unwrap_or_default()
    } else {
        let raw_table: toml::Table = toml::from_str(&content)?;
        if raw_table.contains_key("fp_checker") || raw_table.contains_key("fp-checker") {
            let file: DedicatedConfigFile = toml::from_str(&content)?;
            file.fp_checker.or(file.fp_checker_dash).unwrap_or_default()
        } else {
            toml::from_str::<PartialCheckerConfig>(&content)?
        }
    };

    let mut config = CheckerConfig::default().merge(partial);
    config.base_dir = path.parent().map(Path::to_path_buf);
    config.validate()?;
    Ok(config)
}

pub fn discover_config(target: impl AsRef<Path>) -> Option<PathBuf> {
    let mut cursor = if target.as_ref().is_dir() {
        target.as_ref().to_path_buf()
    } else {
        target.as_ref().parent()?.to_path_buf()
    };

    loop {
        let pyproject = cursor.join("pyproject.toml");
        if pyproject.is_file() {
            return Some(pyproject);
        }

        let checker_toml = cursor.join(".fp-checker.toml");
        if checker_toml.is_file() {
            return Some(checker_toml);
        }

        if !cursor.pop() {
            return None;
        }
    }
}

fn merge_purity_config(base: PurityConfig, partial: Option<PartialPurityConfig>) -> PurityConfig {
    let Some(partial) = partial else {
        return base;
    };

    PurityConfig {
        enable: partial.enable.unwrap_or(base.enable),
        treat_unknown_calls_as: partial
            .treat_unknown_calls_as
            .unwrap_or(base.treat_unknown_calls_as),
        self_attribute_reads_as_external_state: partial
            .self_attribute_reads_as_external_state
            .unwrap_or(base.self_attribute_reads_as_external_state),
        treat_local_mutation_as_impurity: partial
            .treat_local_mutation_as_impurity
            .unwrap_or(base.treat_local_mutation_as_impurity),
        severity: merge_purity_severity_config(base.severity, partial.severity),
    }
}

fn merge_style_config(base: StyleConfig, partial: Option<PartialStyleConfig>) -> StyleConfig {
    let Some(partial) = partial else {
        return base;
    };

    StyleConfig {
        enable: partial.enable.unwrap_or(base.enable),
        reassignment_threshold: partial
            .reassignment_threshold
            .unwrap_or(base.reassignment_threshold),
        mutation_threshold: partial
            .mutation_threshold
            .unwrap_or(base.mutation_threshold),
        imperative_style_threshold: partial
            .imperative_style_threshold
            .unwrap_or(base.imperative_style_threshold),
        severity: merge_style_severity_config(base.severity, partial.severity),
    }
}

fn merge_allowlists_config(
    base: &AllowlistsConfig,
    partial: Option<AllowlistsConfig>,
) -> AllowlistsConfig {
    partial.unwrap_or_else(|| base.clone())
}

fn merge_purity_severity_config(
    base: PuritySeverityConfig,
    partial: Option<PartialPuritySeverityConfig>,
) -> PuritySeverityConfig {
    let Some(partial) = partial else {
        return base;
    };

    PuritySeverityConfig {
        pure: partial.pure.unwrap_or(base.pure),
        impure_reads_external_state: partial
            .impure_reads_external_state
            .unwrap_or(base.impure_reads_external_state),
        impure_writes_external_state: partial
            .impure_writes_external_state
            .unwrap_or(base.impure_writes_external_state),
        impure_direct_io: partial.impure_direct_io.unwrap_or(base.impure_direct_io),
        impure_nondeterministic: partial
            .impure_nondeterministic
            .unwrap_or(base.impure_nondeterministic),
        impure_unknown_call: partial
            .impure_unknown_call
            .unwrap_or(base.impure_unknown_call),
        impure_exceptional_control_dependency: partial
            .impure_exceptional_control_dependency
            .unwrap_or(base.impure_exceptional_control_dependency),
    }
}

fn merge_style_severity_config(
    base: StyleSeverityConfig,
    partial: Option<PartialStyleSeverityConfig>,
) -> StyleSeverityConfig {
    let Some(partial) = partial else {
        return base;
    };

    StyleSeverityConfig {
        no_reassignment: partial.no_reassignment.unwrap_or(base.no_reassignment),
        has_reassignment: partial.has_reassignment.unwrap_or(base.has_reassignment),
        reassignment_heavy: partial
            .reassignment_heavy
            .unwrap_or(base.reassignment_heavy),
        has_mutation: partial.has_mutation.unwrap_or(base.has_mutation),
        mutation_heavy: partial.mutation_heavy.unwrap_or(base.mutation_heavy),
        imperative_style_high: partial
            .imperative_style_high
            .unwrap_or(base.imperative_style_high),
    }
}
