use crate::cli::render::{Icons, Theme};
use serde::Serialize;

use crate::core::config::Config;
use crate::core::db::{create_connection, get_schema_version, verify_pragmas};
use crate::core::error::LoreError;
use crate::core::paths::{get_config_path, get_db_path, get_log_dir};
use crate::gitlab::GitLabClient;

#[derive(Debug, Clone, Serialize)]
pub struct CheckResult {
    pub status: CheckStatus,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
}

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

#[derive(Debug, Serialize)]
pub struct DoctorResult {
    pub success: bool,
    pub checks: DoctorChecks,
}

#[derive(Debug, Serialize)]
pub struct DoctorChecks {
    pub config: ConfigCheck,
    pub database: DatabaseCheck,
    pub gitlab: GitLabCheck,
    pub projects: ProjectsCheck,
    pub ollama: OllamaCheck,
    pub logging: LoggingCheck,
}

#[derive(Debug, Serialize)]
pub struct ConfigCheck {
    #[serde(flatten)]
    pub result: CheckResult,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub path: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct DatabaseCheck {
    #[serde(flatten)]
    pub result: CheckResult,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub path: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub schema_version: Option<i32>,
}

#[derive(Debug, Serialize)]
pub struct GitLabCheck {
    #[serde(flatten)]
    pub result: CheckResult,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub username: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct ProjectsCheck {
    #[serde(flatten)]
    pub result: CheckResult,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub configured: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub resolved: Option<i64>,
}

#[derive(Debug, Serialize)]
pub struct OllamaCheck {
    #[serde(flatten)]
    pub result: CheckResult,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct LoggingCheck {
    #[serde(flatten)]
    pub result: CheckResult,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub log_dir: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub file_count: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub total_bytes: Option<u64>,
}

pub async fn run_doctor(config_path: Option<&str>) -> DoctorResult {
    let config_path_buf = get_config_path(config_path);
    let config_path_str = config_path_buf.display().to_string();

    let (config_check, config) = check_config(&config_path_str);

    let database_check = check_database(config.as_ref());

    let gitlab_check = check_gitlab(config.as_ref()).await;

    let projects_check = check_projects(config.as_ref());

    let ollama_check = check_ollama(config.as_ref()).await;

    let logging_check = check_logging(config.as_ref());

    let success = config_check.result.status == CheckStatus::Ok
        && database_check.result.status == CheckStatus::Ok
        && gitlab_check.result.status == CheckStatus::Ok
        && projects_check.result.status == CheckStatus::Ok;

    DoctorResult {
        success,
        checks: DoctorChecks {
            config: config_check,
            database: database_check,
            gitlab: gitlab_check,
            projects: projects_check,
            ollama: ollama_check,
            logging: logging_check,
        },
    }
}

fn check_config(config_path: &str) -> (ConfigCheck, Option<Config>) {
    match Config::load(Some(config_path)) {
        Ok(config) => (
            ConfigCheck {
                result: CheckResult {
                    status: CheckStatus::Ok,
                    message: Some(format!("Loaded from {config_path}")),
                },
                path: Some(config_path.to_string()),
            },
            Some(config),
        ),
        Err(LoreError::ConfigNotFound { path }) => (
            ConfigCheck {
                result: CheckResult {
                    status: CheckStatus::Error,
                    message: Some(format!("Config not found at {path}")),
                },
                path: Some(path),
            },
            None,
        ),
        Err(e) => (
            ConfigCheck {
                result: CheckResult {
                    status: CheckStatus::Error,
                    message: Some(e.to_string()),
                },
                path: Some(config_path.to_string()),
            },
            None,
        ),
    }
}

fn check_database(config: Option<&Config>) -> DatabaseCheck {
    let Some(config) = config else {
        return DatabaseCheck {
            result: CheckResult {
                status: CheckStatus::Error,
                message: Some("Config not loaded".to_string()),
            },
            path: None,
            schema_version: None,
        };
    };

    let db_path = get_db_path(config.storage.db_path.as_deref());

    if !db_path.exists() {
        return DatabaseCheck {
            result: CheckResult {
                status: CheckStatus::Error,
                message: Some("Database file not found. Run \"lore init\" first.".to_string()),
            },
            path: Some(db_path.display().to_string()),
            schema_version: None,
        };
    }

    match create_connection(&db_path) {
        Ok(conn) => {
            let schema_version = get_schema_version(&conn);
            let (pragmas_ok, issues) = verify_pragmas(&conn);

            if !pragmas_ok {
                return DatabaseCheck {
                    result: CheckResult {
                        status: CheckStatus::Warning,
                        message: Some(format!("Pragma issues: {}", issues.join(", "))),
                    },
                    path: Some(db_path.display().to_string()),
                    schema_version: Some(schema_version),
                };
            }

            DatabaseCheck {
                result: CheckResult {
                    status: CheckStatus::Ok,
                    message: Some(format!("{} (schema v{schema_version})", db_path.display())),
                },
                path: Some(db_path.display().to_string()),
                schema_version: Some(schema_version),
            }
        }
        Err(e) => DatabaseCheck {
            result: CheckResult {
                status: CheckStatus::Error,
                message: Some(e.to_string()),
            },
            path: Some(db_path.display().to_string()),
            schema_version: None,
        },
    }
}

async fn check_gitlab(config: Option<&Config>) -> GitLabCheck {
    let Some(config) = config else {
        return GitLabCheck {
            result: CheckResult {
                status: CheckStatus::Error,
                message: Some("Config not loaded".to_string()),
            },
            url: None,
            username: None,
        };
    };

    let token = match config.gitlab.resolve_token() {
        Ok(t) => t,
        Err(_) => {
            return GitLabCheck {
                result: CheckResult {
                    status: CheckStatus::Error,
                    message: Some(format!(
                        "Token not set. Run 'lore token set' or export {}.",
                        config.gitlab.token_env_var
                    )),
                },
                url: Some(config.gitlab.base_url.clone()),
                username: None,
            };
        }
    };

    let source = config.gitlab.token_source().unwrap_or("unknown");

    let client = GitLabClient::new(&config.gitlab.base_url, &token, None);

    match client.get_current_user().await {
        Ok(user) => GitLabCheck {
            result: CheckResult {
                status: CheckStatus::Ok,
                message: Some(format!(
                    "{} (authenticated as @{}, token from {source})",
                    config.gitlab.base_url, user.username
                )),
            },
            url: Some(config.gitlab.base_url.clone()),
            username: Some(user.username),
        },
        Err(LoreError::GitLabAuthFailed) => GitLabCheck {
            result: CheckResult {
                status: CheckStatus::Error,
                message: Some("Authentication failed. Check your token.".to_string()),
            },
            url: Some(config.gitlab.base_url.clone()),
            username: None,
        },
        Err(e) => GitLabCheck {
            result: CheckResult {
                status: CheckStatus::Error,
                message: Some(e.to_string()),
            },
            url: Some(config.gitlab.base_url.clone()),
            username: None,
        },
    }
}

fn check_projects(config: Option<&Config>) -> ProjectsCheck {
    let Some(config) = config else {
        return ProjectsCheck {
            result: CheckResult {
                status: CheckStatus::Error,
                message: Some("Config not loaded".to_string()),
            },
            configured: None,
            resolved: None,
        };
    };

    let configured = config.projects.len();
    let db_path = get_db_path(config.storage.db_path.as_deref());

    if !db_path.exists() {
        return ProjectsCheck {
            result: CheckResult {
                status: CheckStatus::Error,
                message: Some("Database not found. Run \"lore init\" first.".to_string()),
            },
            configured: Some(configured),
            resolved: Some(0),
        };
    }

    match create_connection(&db_path) {
        Ok(conn) => {
            let resolved: i64 = conn
                .query_row("SELECT COUNT(*) FROM projects", [], |row| row.get(0))
                .unwrap_or(0);

            if resolved == 0 {
                return ProjectsCheck {
                    result: CheckResult {
                        status: CheckStatus::Error,
                        message: Some(format!(
                            "{configured} configured, 0 resolved. Run \"lore init\" to resolve projects."
                        )),
                    },
                    configured: Some(configured),
                    resolved: Some(resolved),
                };
            }

            if resolved != configured as i64 {
                return ProjectsCheck {
                    result: CheckResult {
                        status: CheckStatus::Warning,
                        message: Some(format!(
                            "{configured} configured, {resolved} resolved (mismatch)"
                        )),
                    },
                    configured: Some(configured),
                    resolved: Some(resolved),
                };
            }

            ProjectsCheck {
                result: CheckResult {
                    status: CheckStatus::Ok,
                    message: Some(format!("{configured} configured, {resolved} resolved")),
                },
                configured: Some(configured),
                resolved: Some(resolved),
            }
        }
        Err(e) => ProjectsCheck {
            result: CheckResult {
                status: CheckStatus::Error,
                message: Some(e.to_string()),
            },
            configured: Some(configured),
            resolved: None,
        },
    }
}

async fn check_ollama(config: Option<&Config>) -> OllamaCheck {
    let Some(config) = config else {
        return OllamaCheck {
            result: CheckResult {
                status: CheckStatus::Warning,
                message: Some("Config not loaded".to_string()),
            },
            url: None,
            model: None,
        };
    };

    let base_url = &config.embedding.base_url;
    let model = &config.embedding.model;

    let client = crate::http::Client::with_timeout(std::time::Duration::from_secs(2));
    let url = format!("{base_url}/api/tags");

    match client.get(&url, &[]).await {
        Ok(response) if response.is_success() => {
            #[derive(serde::Deserialize)]
            struct TagsResponse {
                models: Option<Vec<ModelInfo>>,
            }
            #[derive(serde::Deserialize)]
            struct ModelInfo {
                name: String,
            }

            match response.json::<TagsResponse>() {
                Ok(data) => {
                    let models = data.models.unwrap_or_default();
                    let model_names: Vec<&str> = models
                        .iter()
                        .map(|m| m.name.split(':').next().unwrap_or(&m.name))
                        .collect();

                    let model_base = model.split(':').next().unwrap_or(model);
                    if !model_names.contains(&model_base) {
                        return OllamaCheck {
                            result: CheckResult {
                                status: CheckStatus::Warning,
                                message: Some(format!(
                                    "Model \"{model}\" not found. Available: {}",
                                    if model_names.is_empty() {
                                        "none".to_string()
                                    } else {
                                        model_names.join(", ")
                                    }
                                )),
                            },
                            url: Some(base_url.clone()),
                            model: Some(model.clone()),
                        };
                    }

                    OllamaCheck {
                        result: CheckResult {
                            status: CheckStatus::Ok,
                            message: Some(format!("{base_url} (model: {model})")),
                        },
                        url: Some(base_url.clone()),
                        model: Some(model.clone()),
                    }
                }
                Err(_) => OllamaCheck {
                    result: CheckResult {
                        status: CheckStatus::Warning,
                        message: Some("Could not parse Ollama response".to_string()),
                    },
                    url: Some(base_url.clone()),
                    model: Some(model.clone()),
                },
            }
        }
        Ok(response) => OllamaCheck {
            result: CheckResult {
                status: CheckStatus::Warning,
                message: Some(format!("Ollama responded with {}", response.status)),
            },
            url: Some(base_url.clone()),
            model: Some(model.clone()),
        },
        Err(_) => OllamaCheck {
            result: CheckResult {
                status: CheckStatus::Warning,
                message: Some("Not running (semantic search unavailable)".to_string()),
            },
            url: Some(base_url.clone()),
            model: Some(model.clone()),
        },
    }
}

fn check_logging(config: Option<&Config>) -> LoggingCheck {
    let log_dir = get_log_dir(config.and_then(|c| c.logging.log_dir.as_deref()));
    let log_dir_str = log_dir.display().to_string();

    if !log_dir.exists() {
        return LoggingCheck {
            result: CheckResult {
                status: CheckStatus::Ok,
                message: Some(format!("{log_dir_str} (no log files yet)")),
            },
            log_dir: Some(log_dir_str),
            file_count: Some(0),
            total_bytes: Some(0),
        };
    }

    let mut file_count = 0usize;
    let mut total_bytes = 0u64;

    if let Ok(entries) = std::fs::read_dir(&log_dir) {
        for entry in entries.flatten() {
            let name = entry.file_name();
            let name_str = name.to_string_lossy();
            if name_str.starts_with("lore.") {
                file_count += 1;
                if let Ok(meta) = entry.metadata() {
                    total_bytes += meta.len();
                }
            }
        }
    }

    let size_display = if total_bytes < 1024 {
        format!("{total_bytes} B")
    } else if total_bytes < 1024 * 1024 {
        format!("{:.1} KB", total_bytes as f64 / 1024.0)
    } else {
        format!("{:.1} MB", total_bytes as f64 / (1024.0 * 1024.0))
    };

    LoggingCheck {
        result: CheckResult {
            status: CheckStatus::Ok,
            message: Some(format!(
                "{log_dir_str} ({file_count} files, {size_display})"
            )),
        },
        log_dir: Some(log_dir_str),
        file_count: Some(file_count),
        total_bytes: Some(total_bytes),
    }
}

pub fn print_doctor_results(result: &DoctorResult) {
    println!();

    print_check("Config", &result.checks.config.result);
    print_check("Database", &result.checks.database.result);
    print_check("GitLab", &result.checks.gitlab.result);
    print_check("Projects", &result.checks.projects.result);
    print_check("Ollama", &result.checks.ollama.result);
    print_check("Logging", &result.checks.logging.result);

    // Count statuses
    let checks = [
        &result.checks.config.result,
        &result.checks.database.result,
        &result.checks.gitlab.result,
        &result.checks.projects.result,
        &result.checks.ollama.result,
        &result.checks.logging.result,
    ];
    let passed = checks
        .iter()
        .filter(|c| c.status == CheckStatus::Ok)
        .count();
    let warnings = checks
        .iter()
        .filter(|c| c.status == CheckStatus::Warning)
        .count();
    let failed = checks
        .iter()
        .filter(|c| c.status == CheckStatus::Error)
        .count();

    println!();

    let mut summary_parts = Vec::new();
    if result.success {
        summary_parts.push(Theme::success().render("Ready"));
    } else {
        summary_parts.push(Theme::error().render("Not ready"));
    }
    summary_parts.push(format!("{passed} passed"));
    if warnings > 0 {
        summary_parts.push(Theme::warning().render(&format!("{warnings} warning")));
    }
    if failed > 0 {
        summary_parts.push(Theme::error().render(&format!("{failed} failed")));
    }
    println!("  {}", summary_parts.join("  \u{b7}  "));

    println!();
}

fn print_check(name: &str, result: &CheckResult) {
    let icon = match result.status {
        CheckStatus::Ok => Theme::success().render(Icons::success()),
        CheckStatus::Warning => Theme::warning().render(Icons::warning()),
        CheckStatus::Error => Theme::error().render(Icons::error()),
    };

    let message = result.message.as_deref().unwrap_or("");
    let message_styled = match result.status {
        CheckStatus::Ok => message.to_string(),
        CheckStatus::Warning => Theme::warning().render(message),
        CheckStatus::Error => Theme::error().render(message),
    };

    println!("  {icon}  {:<10} {message_styled}", name);
}
