use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use serde::Serialize;

use crate::config::{self, Config, OAuthFlow, OAuthProfile};
use crate::secrets::SecretManager;
use crate::template::catalog::TemplateCatalog;
use crate::template::loader::{load_catalog_from_dirs, validate_all_from_dirs};
use crate::template::schema::AuthTemplate;

#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum DoctorStatus {
    Ok,
    Warning,
    Error,
}

#[derive(Debug, Clone, Serialize)]
pub struct DoctorCheck {
    pub id: String,
    pub status: DoctorStatus,
    pub message: String,
    pub suggestion: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct DoctorSummary {
    pub ok: usize,
    pub warning: usize,
    pub error: usize,
}

#[derive(Debug, Clone, Default, Serialize)]
pub struct DoctorReport {
    pub checks: Vec<DoctorCheck>,
}

impl DoctorReport {
    pub fn add_ok(&mut self, id: &str, message: impl Into<String>) {
        self.checks.push(DoctorCheck {
            id: id.to_string(),
            status: DoctorStatus::Ok,
            message: message.into(),
            suggestion: None,
        });
    }

    pub fn add_warning(
        &mut self,
        id: &str,
        message: impl Into<String>,
        suggestion: Option<String>,
    ) {
        self.checks.push(DoctorCheck {
            id: id.to_string(),
            status: DoctorStatus::Warning,
            message: message.into(),
            suggestion,
        });
    }

    pub fn add_error(&mut self, id: &str, message: impl Into<String>, suggestion: Option<String>) {
        self.checks.push(DoctorCheck {
            id: id.to_string(),
            status: DoctorStatus::Error,
            message: message.into(),
            suggestion,
        });
    }

    pub fn summary(&self) -> DoctorSummary {
        let mut summary = DoctorSummary {
            ok: 0,
            warning: 0,
            error: 0,
        };
        for check in &self.checks {
            match check.status {
                DoctorStatus::Ok => summary.ok += 1,
                DoctorStatus::Warning => summary.warning += 1,
                DoctorStatus::Error => summary.error += 1,
            }
        }
        summary
    }

    pub fn has_errors(&self) -> bool {
        self.checks
            .iter()
            .any(|check| matches!(check.status, DoctorStatus::Error))
    }

    pub fn error_count(&self) -> usize {
        self.checks
            .iter()
            .filter(|check| matches!(check.status, DoctorStatus::Error))
            .count()
    }
}

pub fn run_checks(cwd: &Path) -> DoctorReport {
    let mut report = DoctorReport::default();
    let config_path = config::config_file_path();

    let cfg = if !config_path.exists() {
        report.add_warning(
            "config_file",
            format!("Config file not found at {}", config_path.display()),
            Some(
                "Create ~/.config/earl/config.toml (for example by copying examples/config.toml)."
                    .to_string(),
            ),
        );
        Config::default()
    } else {
        match config::load_config() {
            Ok(cfg) => {
                report.add_ok(
                    "config_file",
                    format!("Loaded config from {}", config_path.display()),
                );
                cfg
            }
            Err(err) => {
                report.add_error(
                    "config_file",
                    format!("Failed to parse {}: {err:#}", config_path.display()),
                    Some("Fix config TOML syntax, then re-run `earl doctor`.".to_string()),
                );
                Config::default()
            }
        }
    };

    if cfg.network.allow.is_empty() {
        report.add_ok(
            "network_allowlist",
            "No [[network.allow]] entries configured; outbound requests are allowed by default.",
        );
    } else {
        report.add_ok(
            "network_allowlist",
            format!("Configured {} allowlist rule(s).", cfg.network.allow.len()),
        );
    }

    let global_dir = config::global_templates_dir();
    let local_dir = config::local_templates_dir(cwd);

    let mut discovery_failed = false;
    let global_files = match discover_template_files(&global_dir) {
        Ok(files) => files,
        Err(err) => {
            discovery_failed = true;
            report.add_error(
                "templates",
                format!(
                    "Failed listing global templates in {}: {err:#}",
                    global_dir.display()
                ),
                None,
            );
            Vec::new()
        }
    };

    let local_files = match discover_template_files(&local_dir) {
        Ok(files) => files,
        Err(err) => {
            discovery_failed = true;
            report.add_error(
                "templates",
                format!(
                    "Failed listing local templates in {}: {err:#}",
                    local_dir.display()
                ),
                None,
            );
            Vec::new()
        }
    };

    let template_count = global_files.len() + local_files.len();
    if !discovery_failed {
        if template_count == 0 {
            report.add_warning(
                "templates",
                format!(
                    "No template files found in {} or {}.",
                    local_dir.display(),
                    global_dir.display()
                ),
                Some(
                    "Add a .hcl template under ./templates or ~/.config/earl/templates."
                        .to_string(),
                ),
            );
        } else {
            report.add_ok(
                "templates",
                format!(
                    "Found {} template file(s) (local: {}, global: {}).",
                    template_count,
                    local_files.len(),
                    global_files.len()
                ),
            );
        }
    }

    if !discovery_failed && template_count > 0 {
        match validate_all_from_dirs(&global_dir, &local_dir) {
            Ok(files) => report.add_ok(
                "template_validation",
                format!("Validated {} template file(s).", files.len()),
            ),
            Err(err) => report.add_error(
                "template_validation",
                format!("Template validation failed: {err:#}"),
                Some("Run `earl templates validate` for details.".to_string()),
            ),
        }
    }

    let mut catalog = None;
    if !discovery_failed {
        match load_catalog_from_dirs(&global_dir, &local_dir) {
            Ok(found) => {
                if found.entries.is_empty() {
                    report.add_warning(
                        "commands",
                        "No commands available after loading templates.".to_string(),
                        Some(
                            "Ensure template files define at least one `command \"<name>\" { ... }` block."
                                .to_string(),
                        ),
                    );
                } else {
                    report.add_ok(
                        "commands",
                        format!("Loaded {} command(s).", found.entries.len()),
                    );
                }
                catalog = Some(found);
            }
            Err(err) => {
                report.add_error(
                    "commands",
                    format!("Failed loading template catalog: {err:#}"),
                    None,
                );
            }
        }
    }

    if let Some(catalog) = &catalog {
        check_required_secrets(catalog, &mut report);
        check_oauth_profiles(catalog, &cfg, &mut report);
    }

    check_remote_search(&cfg, &mut report);

    report
}

fn check_required_secrets(catalog: &TemplateCatalog, report: &mut DoctorReport) {
    let mut required = BTreeSet::new();
    for entry in catalog.values() {
        for secret in &entry.template.annotations.secrets {
            required.insert(secret.clone());
        }
    }

    if required.is_empty() {
        report.add_ok("required_secrets", "Templates do not require any secrets.");
        return;
    }

    let manager = SecretManager::new();
    let mut missing = Vec::new();

    for key in &required {
        match manager.store().get_secret(key) {
            Ok(Some(_)) => {}
            Ok(None) => missing.push(key.clone()),
            Err(err) => {
                report.add_warning(
                    "required_secrets",
                    format!("Could not verify keychain secrets: {err:#}"),
                    Some(
                        "Ensure keychain access is available, then run `earl doctor` again."
                            .to_string(),
                    ),
                );
                return;
            }
        }
    }

    if missing.is_empty() {
        report.add_ok(
            "required_secrets",
            format!("All {} required secret(s) are present.", required.len()),
        );
    } else {
        report.add_warning(
            "required_secrets",
            format!(
                "Missing {} required secret(s): {}",
                missing.len(),
                summarize_items(&missing, 5)
            ),
            Some("Set missing values with `earl secrets set <key>`.".to_string()),
        );
    }
}

fn check_oauth_profiles(catalog: &TemplateCatalog, cfg: &Config, report: &mut DoctorReport) {
    let mut referenced = BTreeSet::new();
    for entry in catalog.values() {
        if let Some(AuthTemplate::OAuth2Profile { profile }) = entry.template.operation.auth() {
            referenced.insert(profile.clone());
        }
    }

    if referenced.is_empty() {
        report.add_ok(
            "oauth_profiles",
            "Templates do not reference any oauth2_profile auth settings.",
        );
        return;
    }

    let mut missing = Vec::new();
    let mut invalid = Vec::new();

    for profile_name in &referenced {
        let Some(profile) = cfg.auth.profiles.get(profile_name) else {
            missing.push(profile_name.clone());
            continue;
        };

        let issues = validate_profile(profile);
        if !issues.is_empty() {
            invalid.push(format!("{} ({})", profile_name, issues.join("; ")));
        }
    }

    if missing.is_empty() && invalid.is_empty() {
        report.add_ok(
            "oauth_profiles",
            format!(
                "All {} referenced OAuth profile(s) are configured.",
                referenced.len()
            ),
        );
        return;
    }

    let mut parts = Vec::new();
    if !missing.is_empty() {
        parts.push(format!(
            "missing profiles: {}",
            summarize_items(&missing, 5)
        ));
    }
    if !invalid.is_empty() {
        parts.push(format!(
            "invalid profiles: {}",
            summarize_items(&invalid, 3)
        ));
    }

    report.add_error(
        "oauth_profiles",
        parts.join("; "),
        Some(
            "Add or fix entries under [auth.profiles.<name>] in ~/.config/earl/config.toml."
                .to_string(),
        ),
    );
}

fn validate_profile(profile: &OAuthProfile) -> Vec<String> {
    let mut issues = Vec::new();

    if profile.client_id.trim().is_empty() {
        issues.push("client_id is empty".to_string());
    }

    let has_issuer = profile
        .issuer
        .as_deref()
        .map(|value| !value.trim().is_empty())
        .unwrap_or(false);

    if missing_text(&profile.token_url) && !has_issuer {
        issues.push("missing token_url (or issuer for discovery)".to_string());
    }

    match profile.flow {
        OAuthFlow::AuthCodePkce => {
            if missing_text(&profile.authorization_url) && !has_issuer {
                issues.push("missing authorization_url (or issuer for discovery)".to_string());
            }
        }
        OAuthFlow::DeviceCode => {
            if missing_text(&profile.device_authorization_url) && !has_issuer {
                issues
                    .push("missing device_authorization_url (or issuer for discovery)".to_string());
            }
        }
        OAuthFlow::ClientCredentials => {}
    }

    issues
}

fn check_remote_search(cfg: &Config, report: &mut DoctorReport) {
    let remote = &cfg.search.remote;
    if !remote.enabled {
        report.add_ok("remote_search", "Remote semantic search is disabled.");
        return;
    }

    let mut issues = Vec::new();
    if missing_text(&remote.base_url) {
        issues.push("base_url is not set".to_string());
    }
    if missing_text(&remote.api_key_secret) {
        issues.push("api_key_secret is not set".to_string());
    }

    if !issues.is_empty() {
        report.add_warning(
            "remote_search",
            format!("Remote semantic search enabled but {}", issues.join(", ")),
            Some("Set [search.remote].base_url and [search.remote].api_key_secret.".to_string()),
        );
        return;
    }

    let manager = SecretManager::new();
    let secret_key = remote.api_key_secret.as_ref().expect("checked above");
    match manager.store().get_secret(secret_key) {
        Ok(Some(_)) => report.add_ok(
            "remote_search",
            format!(
                "Remote semantic search is configured (base_url: {}).",
                remote.base_url.as_deref().unwrap_or_default()
            ),
        ),
        Ok(None) => report.add_warning(
            "remote_search",
            format!("Remote search API key secret `{secret_key}` is missing."),
            Some(format!("Set it with `earl secrets set {secret_key}`.")),
        ),
        Err(err) => report.add_warning(
            "remote_search",
            format!("Could not verify remote search API key secret: {err:#}"),
            Some("Ensure keychain access is available, then run `earl doctor` again.".to_string()),
        ),
    }
}

fn discover_template_files(dir: &Path) -> Result<Vec<PathBuf>> {
    if !dir.exists() {
        return Ok(Vec::new());
    }

    let mut files = Vec::new();
    collect_discovered_template_files(dir, &mut files)?;

    files.sort();
    Ok(files)
}

fn collect_discovered_template_files(dir: &Path, files: &mut Vec<PathBuf>) -> Result<()> {
    for entry in fs::read_dir(dir)
        .with_context(|| format!("failed listing template directory {}", dir.display()))?
    {
        let entry = entry?;
        let path = entry.path();
        let file_type = entry
            .file_type()
            .with_context(|| format!("failed inspecting template path {}", path.display()))?;
        if file_type.is_dir() {
            collect_discovered_template_files(&path, files)?;
            continue;
        }
        if file_type.is_file() && is_template_file(&path) {
            files.push(path);
        }
    }

    Ok(())
}

fn is_template_file(path: &Path) -> bool {
    path.extension()
        .and_then(|ext| ext.to_str())
        .map(|ext| ext == "hcl")
        .unwrap_or(false)
}

fn missing_text(value: &Option<String>) -> bool {
    value
        .as_deref()
        .map(|text| text.trim().is_empty())
        .unwrap_or(true)
}

fn summarize_items(items: &[String], limit: usize) -> String {
    let mut parts = Vec::new();
    for item in items.iter().take(limit) {
        parts.push(item.clone());
    }

    if items.len() > limit {
        parts.push(format!("... +{} more", items.len() - limit));
    }

    parts.join(", ")
}
