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

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

impl Default for OutputFormat {
    fn default() -> Self {
        Self::Text
    }
}

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 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,
            custom_rules: Vec::new(),
        }
    }
}

impl CheckerConfig {
    #[must_use]
    pub fn is_rule_enabled(&self, rule_id: &RuleId) -> bool {
        self.enabled_rules
            .as_ref()
            .map_or(true, |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(),
            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, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CustomRuleMatchType {
    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,
}

impl Default for CustomRuleMatchType {
    fn default() -> Self {
        Self::Exact
    }
}

fn default_custom_rule_enabled() -> bool {
    true
}

#[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 custom_rules: Option<Vec<CustomRuleConfig>>,
}

#[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;
        }
    }
}
