use std::fs;
use std::io::{IsTerminal, Read};

use crate::core::config::{Config, MinimalConfig, MinimalGitLabConfig, ProjectConfig};
use crate::core::db::{create_connection, run_migrations};
use crate::core::error::{LoreError, Result};
use crate::core::paths::{ensure_config_permissions, get_config_path, get_data_dir};
use crate::gitlab::{GitLabClient, GitLabProject};

pub struct InitInputs {
    pub gitlab_url: String,
    pub token_env_var: String,
    pub project_paths: Vec<String>,
    pub default_project: Option<String>,
}

pub struct InitOptions {
    pub config_path: Option<String>,
    pub force: bool,
    pub non_interactive: bool,
}

pub struct InitResult {
    pub config_path: String,
    pub data_dir: String,
    pub user: UserInfo,
    pub projects: Vec<ProjectInfo>,
    pub default_project: Option<String>,
}

pub struct UserInfo {
    pub username: String,
    pub name: String,
}

pub struct ProjectInfo {
    pub path: String,
    pub name: String,
}

// ── Refresh types ──

pub struct RefreshOptions {
    pub config_path: Option<String>,
    pub non_interactive: bool,
}

pub struct RefreshResult {
    pub user: UserInfo,
    pub projects_registered: Vec<ProjectInfo>,
    pub projects_failed: Vec<ProjectFailure>,
    pub orphans_found: Vec<String>,
    pub orphans_deleted: Vec<String>,
}

pub struct ProjectFailure {
    pub path: String,
    pub error: String,
}

/// Re-read existing config and register any new projects in the database.
/// Does NOT modify the config file.
pub async fn run_init_refresh(options: RefreshOptions) -> Result<RefreshResult> {
    let config_path = get_config_path(options.config_path.as_deref());

    if !config_path.exists() {
        return Err(LoreError::ConfigNotFound {
            path: config_path.display().to_string(),
        });
    }

    let config = Config::load(options.config_path.as_deref())?;
    let token = config.gitlab.resolve_token()?;
    let client = GitLabClient::new(&config.gitlab.base_url, &token, None);

    // Validate auth
    let gitlab_user = client.get_current_user().await.map_err(|e| {
        if matches!(e, LoreError::GitLabAuthFailed) {
            LoreError::Other(format!(
                "Authentication failed for {}",
                config.gitlab.base_url
            ))
        } else {
            e
        }
    })?;

    let user = UserInfo {
        username: gitlab_user.username,
        name: gitlab_user.name,
    };

    // Validate each project
    let mut validated_projects: Vec<(ProjectInfo, GitLabProject)> = Vec::new();
    let mut failed_projects: Vec<ProjectFailure> = Vec::new();

    for project_config in &config.projects {
        match client.get_project(&project_config.path).await {
            Ok(project) => {
                validated_projects.push((
                    ProjectInfo {
                        path: project_config.path.clone(),
                        name: project.name.clone().unwrap_or_else(|| {
                            project_config
                                .path
                                .split('/')
                                .next_back()
                                .unwrap_or(&project_config.path)
                                .to_string()
                        }),
                    },
                    project,
                ));
            }
            Err(e) => {
                let error_msg = if matches!(e, LoreError::GitLabNotFound { .. }) {
                    "not found".to_string()
                } else {
                    e.to_string()
                };
                failed_projects.push(ProjectFailure {
                    path: project_config.path.clone(),
                    error: error_msg,
                });
            }
        }
    }

    // Open database
    let data_dir = get_data_dir();
    let db_path = data_dir.join("lore.db");
    let conn = create_connection(&db_path)?;
    run_migrations(&conn)?;

    // Find orphans: projects in DB but not in config
    let config_paths: std::collections::HashSet<&str> =
        config.projects.iter().map(|p| p.path.as_str()).collect();

    let mut stmt = conn.prepare("SELECT path_with_namespace FROM projects")?;
    let db_projects: Vec<String> = stmt
        .query_map([], |row| row.get(0))?
        .filter_map(|r| r.ok())
        .collect();

    let orphans: Vec<String> = db_projects
        .into_iter()
        .filter(|p| !config_paths.contains(p.as_str()))
        .collect();

    // Upsert validated projects
    for (_, gitlab_project) in &validated_projects {
        conn.execute(
            "INSERT INTO projects (gitlab_project_id, path_with_namespace, default_branch, web_url)
             VALUES (?, ?, ?, ?)
             ON CONFLICT(gitlab_project_id) DO UPDATE SET
               path_with_namespace = excluded.path_with_namespace,
               default_branch = excluded.default_branch,
               web_url = excluded.web_url",
            (
                gitlab_project.id,
                &gitlab_project.path_with_namespace,
                &gitlab_project.default_branch,
                &gitlab_project.web_url,
            ),
        )?;
    }

    Ok(RefreshResult {
        user,
        projects_registered: validated_projects.into_iter().map(|(p, _)| p).collect(),
        projects_failed: failed_projects,
        orphans_found: orphans,
        orphans_deleted: Vec::new(), // Caller handles deletion after user prompt
    })
}

/// Delete orphan projects from the database.
pub fn delete_orphan_projects(config_path: Option<&str>, orphans: &[String]) -> Result<usize> {
    let data_dir = get_data_dir();
    let db_path = data_dir.join("lore.db");
    let conn = create_connection(&db_path)?;

    let _ = config_path; // Reserved for future use

    let mut deleted = 0;
    for path in orphans {
        let rows = conn.execute("DELETE FROM projects WHERE path_with_namespace = ?", [path])?;
        deleted += rows;
    }

    Ok(deleted)
}

pub async fn run_init(inputs: InitInputs, options: InitOptions) -> Result<InitResult> {
    let config_path = get_config_path(options.config_path.as_deref());
    let data_dir = get_data_dir();

    if config_path.exists() && !options.force {
        if options.non_interactive {
            return Err(LoreError::Other(format!(
                "Config file exists at {}. Use --force to overwrite.",
                config_path.display()
            )));
        }

        return Err(LoreError::Other(
            "User cancelled config overwrite.".to_string(),
        ));
    }

    if url::Url::parse(&inputs.gitlab_url).is_err() {
        return Err(LoreError::Other(format!(
            "Invalid GitLab URL: {}",
            inputs.gitlab_url
        )));
    }

    let token = std::env::var(&inputs.token_env_var).map_err(|_| LoreError::TokenNotSet {
        env_var: inputs.token_env_var.clone(),
    })?;

    let client = GitLabClient::new(&inputs.gitlab_url, &token, None);

    let gitlab_user = client.get_current_user().await.map_err(|e| {
        if matches!(e, LoreError::GitLabAuthFailed) {
            LoreError::Other(format!("Authentication failed for {}", inputs.gitlab_url))
        } else {
            e
        }
    })?;

    let user = UserInfo {
        username: gitlab_user.username,
        name: gitlab_user.name,
    };

    let mut validated_projects: Vec<(ProjectInfo, GitLabProject)> = Vec::new();

    for project_path in &inputs.project_paths {
        let project = client.get_project(project_path).await.map_err(|e| {
            if matches!(e, LoreError::GitLabNotFound { .. }) {
                LoreError::Other(format!("Project not found: {project_path}"))
            } else {
                e
            }
        })?;

        validated_projects.push((
            ProjectInfo {
                path: project_path.clone(),
                name: project.name.clone().unwrap_or_else(|| {
                    project_path
                        .split('/')
                        .next_back()
                        .unwrap_or(project_path)
                        .to_string()
                }),
            },
            project,
        ));
    }

    // Validate default_project matches one of the configured project paths
    if let Some(ref dp) = inputs.default_project {
        let matched = inputs.project_paths.iter().any(|p| {
            p.eq_ignore_ascii_case(dp)
                || p.to_ascii_lowercase()
                    .ends_with(&format!("/{}", dp.to_ascii_lowercase()))
        });
        if !matched {
            return Err(LoreError::Other(format!(
                "defaultProject '{dp}' does not match any configured project path"
            )));
        }
    }

    if let Some(parent) = config_path.parent() {
        fs::create_dir_all(parent)?;
    }

    let config = MinimalConfig {
        gitlab: MinimalGitLabConfig {
            base_url: inputs.gitlab_url,
            token_env_var: inputs.token_env_var,
        },
        projects: inputs
            .project_paths
            .iter()
            .map(|p| ProjectConfig { path: p.clone() })
            .collect(),
        default_project: inputs.default_project.clone(),
    };

    let config_json = serde_json::to_string_pretty(&config)?;
    fs::write(&config_path, format!("{config_json}\n"))?;

    fs::create_dir_all(&data_dir)?;

    let db_path = data_dir.join("lore.db");
    let conn = create_connection(&db_path)?;

    run_migrations(&conn)?;

    for (_, gitlab_project) in &validated_projects {
        conn.execute(
            "INSERT INTO projects (gitlab_project_id, path_with_namespace, default_branch, web_url)
             VALUES (?, ?, ?, ?)
             ON CONFLICT(gitlab_project_id) DO UPDATE SET
               path_with_namespace = excluded.path_with_namespace,
               default_branch = excluded.default_branch,
               web_url = excluded.web_url",
            (
                gitlab_project.id,
                &gitlab_project.path_with_namespace,
                &gitlab_project.default_branch,
                &gitlab_project.web_url,
            ),
        )?;
    }

    Ok(InitResult {
        config_path: config_path.display().to_string(),
        data_dir: data_dir.display().to_string(),
        user,
        projects: validated_projects.into_iter().map(|(p, _)| p).collect(),
        default_project: inputs.default_project,
    })
}

// ── token set / show ──

pub struct TokenSetResult {
    pub username: String,
    pub config_path: String,
}

pub struct TokenShowResult {
    pub token: String,
    pub source: &'static str,
}

/// Read token from --token flag or stdin, validate against GitLab, store in config.
pub async fn run_token_set(
    config_path_override: Option<&str>,
    token_arg: Option<String>,
) -> Result<TokenSetResult> {
    let config_path = get_config_path(config_path_override);

    if !config_path.exists() {
        return Err(LoreError::ConfigNotFound {
            path: config_path.display().to_string(),
        });
    }

    // Resolve token value: flag > stdin > error
    let token = if let Some(t) = token_arg {
        t.trim().to_string()
    } else if !std::io::stdin().is_terminal() {
        let mut buf = String::new();
        std::io::stdin()
            .read_to_string(&mut buf)
            .map_err(|e| LoreError::Other(format!("Failed to read token from stdin: {e}")))?;
        buf.trim().to_string()
    } else {
        return Err(LoreError::Other(
            "No token provided. Use --token or pipe to stdin.".to_string(),
        ));
    };

    if token.is_empty() {
        return Err(LoreError::Other("Token cannot be empty.".to_string()));
    }

    // Load config to get the base URL for validation
    let config = Config::load(config_path_override)?;

    // Validate token against GitLab
    let client = GitLabClient::new(&config.gitlab.base_url, &token, None);
    let user = client.get_current_user().await.map_err(|e| {
        if matches!(e, LoreError::GitLabAuthFailed) {
            LoreError::Other("Token validation failed: authentication rejected by GitLab.".into())
        } else {
            e
        }
    })?;

    // Read config as raw JSON, insert token, write back
    let content = fs::read_to_string(&config_path)
        .map_err(|e| LoreError::Other(format!("Failed to read config file: {e}")))?;

    let mut json: serde_json::Value =
        serde_json::from_str(&content).map_err(|e| LoreError::ConfigInvalid {
            details: format!("Invalid JSON in config file: {e}"),
        })?;

    json["gitlab"]["token"] = serde_json::Value::String(token);

    let output = serde_json::to_string_pretty(&json)
        .map_err(|e| LoreError::Other(format!("Failed to serialize config: {e}")))?;
    fs::write(&config_path, format!("{output}\n"))?;

    // Enforce permissions
    ensure_config_permissions(&config_path);

    Ok(TokenSetResult {
        username: user.username,
        config_path: config_path.display().to_string(),
    })
}

/// Show the current token (masked or unmasked) and its source.
pub fn run_token_show(config_path_override: Option<&str>, unmask: bool) -> Result<TokenShowResult> {
    let config = Config::load(config_path_override)?;

    let source = config
        .gitlab
        .token_source()
        .ok_or_else(|| LoreError::TokenNotSet {
            env_var: config.gitlab.token_env_var.clone(),
        })?;

    let token = config.gitlab.resolve_token()?;

    let display_token = if unmask { token } else { mask_token(&token) };

    Ok(TokenShowResult {
        token: display_token,
        source,
    })
}

fn mask_token(token: &str) -> String {
    let len = token.len();
    if len <= 8 {
        "*".repeat(len)
    } else {
        let visible = &token[..4];
        format!("{visible}{}", "*".repeat(len - 4))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn mask_token_hides_short_tokens_completely() {
        assert_eq!(mask_token(""), "");
        assert_eq!(mask_token("a"), "*");
        assert_eq!(mask_token("abcd"), "****");
        assert_eq!(mask_token("abcdefgh"), "********");
    }

    #[test]
    fn mask_token_reveals_first_four_chars_for_long_tokens() {
        assert_eq!(mask_token("abcdefghi"), "abcd*****");
        assert_eq!(mask_token("glpat-xyzABC123456"), "glpa**************");
    }

    #[test]
    fn mask_token_boundary_at_nine_chars() {
        // 8 chars → fully masked, 9 chars → first 4 visible
        assert_eq!(mask_token("12345678"), "********");
        assert_eq!(mask_token("123456789"), "1234*****");
    }
}
