use serde::Serialize;

use tracing::info;

use crate::Config;
use crate::cli::render::{self, Icons, Theme};
use crate::core::db::create_connection;
use crate::core::error::{LoreError, Result};
use crate::core::file_history::resolve_rename_chain;
use crate::core::paths::get_db_path;
use crate::core::project::resolve_project;
use crate::core::time::ms_to_iso;

/// Maximum rename chain BFS depth.
const MAX_RENAME_HOPS: usize = 10;

/// A single MR that touched the file.
#[derive(Debug, Serialize)]
pub struct FileHistoryMr {
    pub iid: i64,
    pub title: String,
    pub state: String,
    pub author_username: String,
    pub change_type: String,
    pub merged_at_iso: Option<String>,
    pub updated_at_iso: String,
    pub merge_commit_sha: Option<String>,
    pub web_url: Option<String>,
}

/// A DiffNote discussion snippet on the file.
#[derive(Debug, Serialize)]
pub struct FileDiscussion {
    pub discussion_id: String,
    pub author_username: String,
    pub body_snippet: String,
    pub path: String,
    pub created_at_iso: String,
}

/// Full result of a file-history query.
#[derive(Debug, Serialize)]
pub struct FileHistoryResult {
    pub path: String,
    pub rename_chain: Vec<String>,
    pub renames_followed: bool,
    pub merge_requests: Vec<FileHistoryMr>,
    pub discussions: Vec<FileDiscussion>,
    pub total_mrs: usize,
    pub paths_searched: usize,
    /// Diagnostic hints explaining why results may be empty.
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub hints: Vec<String>,
}

/// Run the file-history query.
pub fn run_file_history(
    config: &Config,
    path: &str,
    project: Option<&str>,
    no_follow_renames: bool,
    merged_only: bool,
    include_discussions: bool,
    limit: usize,
) -> Result<FileHistoryResult> {
    let db_path = get_db_path(config.storage.db_path.as_deref());
    let conn = create_connection(&db_path)?;

    let project_id = project.map(|p| resolve_project(&conn, p)).transpose()?;

    // Resolve rename chain unless disabled
    let (all_paths, renames_followed) = if no_follow_renames {
        (vec![path.to_string()], false)
    } else if let Some(pid) = project_id {
        let chain = resolve_rename_chain(&conn, pid, path, MAX_RENAME_HOPS)?;
        let followed = chain.len() > 1;
        (chain, followed)
    } else {
        // Without a project scope, can't resolve renames (need project_id)
        (vec![path.to_string()], false)
    };

    let paths_searched = all_paths.len();

    info!(
        paths = paths_searched,
        renames_followed, "file-history: resolved {} path(s) for '{}'", paths_searched, path
    );

    // Build placeholders for IN clause
    let placeholders: Vec<String> = (0..all_paths.len())
        .map(|i| format!("?{}", i + 2))
        .collect();
    let in_clause = placeholders.join(", ");

    let merged_filter = if merged_only {
        " AND mr.state = 'merged'"
    } else {
        ""
    };

    let project_filter = if project_id.is_some() {
        "AND mfc.project_id = ?1"
    } else {
        ""
    };

    let sql = format!(
        "SELECT DISTINCT \
            mr.iid, mr.title, mr.state, mr.author_username, \
            mfc.change_type, mr.merged_at, mr.updated_at, mr.merge_commit_sha, mr.web_url \
         FROM mr_file_changes mfc \
         JOIN merge_requests mr ON mr.id = mfc.merge_request_id \
         WHERE mfc.new_path IN ({in_clause}) {project_filter} {merged_filter} \
         ORDER BY COALESCE(mr.merged_at, mr.updated_at) DESC \
         LIMIT ?{}",
        all_paths.len() + 2
    );

    let mut stmt = conn.prepare(&sql)?;

    // Bind parameters: ?1 = project_id (or 0 placeholder), ?2..?N+1 = paths, ?N+2 = limit
    let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
    params.push(Box::new(project_id.unwrap_or(0)));
    for p in &all_paths {
        params.push(Box::new(p.clone()));
    }
    params.push(Box::new(limit as i64));

    let param_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect();

    let merge_requests: Vec<FileHistoryMr> = stmt
        .query_map(param_refs.as_slice(), |row| {
            let merged_at: Option<i64> = row.get(5)?;
            let updated_at: i64 = row.get(6)?;
            Ok(FileHistoryMr {
                iid: row.get(0)?,
                title: row.get(1)?,
                state: row.get(2)?,
                author_username: row.get(3)?,
                change_type: row.get(4)?,
                merged_at_iso: merged_at.map(ms_to_iso),
                updated_at_iso: ms_to_iso(updated_at),
                merge_commit_sha: row.get(7)?,
                web_url: row.get(8)?,
            })
        })?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    let total_mrs = merge_requests.len();

    info!(
        mr_count = total_mrs,
        "file-history: found {} MR(s) touching '{}'", total_mrs, path
    );

    // Optionally fetch DiffNote discussions on this file
    let discussions = if include_discussions && !merge_requests.is_empty() {
        let discs = fetch_file_discussions(&conn, &all_paths, project_id)?;
        info!(
            discussion_count = discs.len(),
            "file-history: found {} discussion(s)",
            discs.len()
        );
        discs
    } else {
        Vec::new()
    };

    // Build diagnostic hints when no results found
    let hints = if total_mrs == 0 {
        build_file_history_hints(&conn, project_id, &all_paths)?
    } else {
        Vec::new()
    };

    Ok(FileHistoryResult {
        path: path.to_string(),
        rename_chain: all_paths,
        renames_followed,
        merge_requests,
        discussions,
        total_mrs,
        paths_searched,
        hints,
    })
}

/// Fetch DiffNote discussions that reference the given file paths.
fn fetch_file_discussions(
    conn: &rusqlite::Connection,
    paths: &[String],
    project_id: Option<i64>,
) -> Result<Vec<FileDiscussion>> {
    let placeholders: Vec<String> = (0..paths.len()).map(|i| format!("?{}", i + 2)).collect();
    let in_clause = placeholders.join(", ");

    let project_filter = if project_id.is_some() {
        "AND d.project_id = ?1"
    } else {
        ""
    };

    let sql = format!(
        "SELECT d.gitlab_discussion_id, n.author_username, n.body, n.position_new_path, n.created_at \
         FROM notes n \
         JOIN discussions d ON d.id = n.discussion_id \
         WHERE n.position_new_path IN ({in_clause}) {project_filter} \
           AND n.is_system = 0 \
         ORDER BY n.created_at DESC"
    );

    let mut stmt = conn.prepare(&sql)?;

    let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
    params.push(Box::new(project_id.unwrap_or(0)));
    for p in paths {
        params.push(Box::new(p.clone()));
    }

    let param_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect();

    let discussions: Vec<FileDiscussion> = stmt
        .query_map(param_refs.as_slice(), |row| {
            let body: String = row.get(2)?;
            let snippet = if body.len() > 200 {
                format!("{}...", &body[..body.floor_char_boundary(200)])
            } else {
                body
            };
            let created_at: i64 = row.get(4)?;
            Ok(FileDiscussion {
                discussion_id: row.get(0)?,
                author_username: row.get(1)?,
                body_snippet: snippet,
                path: row.get(3)?,
                created_at_iso: ms_to_iso(created_at),
            })
        })?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    Ok(discussions)
}

/// Build diagnostic hints explaining why a file-history query returned no results.
fn build_file_history_hints(
    conn: &rusqlite::Connection,
    project_id: Option<i64>,
    paths: &[String],
) -> Result<Vec<String>> {
    let mut hints = Vec::new();

    // Check if mr_file_changes has ANY rows for this project
    let has_file_changes: bool = if let Some(pid) = project_id {
        conn.query_row(
            "SELECT EXISTS(SELECT 1 FROM mr_file_changes WHERE project_id = ?1 LIMIT 1)",
            rusqlite::params![pid],
            |row| row.get(0),
        )?
    } else {
        conn.query_row(
            "SELECT EXISTS(SELECT 1 FROM mr_file_changes LIMIT 1)",
            [],
            |row| row.get(0),
        )?
    };

    if !has_file_changes {
        hints.push(
            "No MR file changes have been synced yet. Run 'lore sync' to fetch file change data."
                .to_string(),
        );
        return Ok(hints);
    }

    // File changes exist but none match these paths
    let path_list = paths
        .iter()
        .map(|p| format!("'{p}'"))
        .collect::<Vec<_>>()
        .join(", ");
    hints.push(format!(
        "Searched paths [{}] were not found in MR file changes. \
         The file may predate the sync window or use a different path.",
        path_list
    ));

    Ok(hints)
}

// ── Human output ────────────────────────────────────────────────────────────

pub fn print_file_history(result: &FileHistoryResult) {
    // Header
    let paths_info = if result.paths_searched > 1 {
        format!(
            " (via {} paths, {} MRs)",
            result.paths_searched, result.total_mrs
        )
    } else {
        format!(" ({} MRs)", result.total_mrs)
    };

    println!();
    println!(
        "{}",
        Theme::bold().render(&format!("File History: {}{}", result.path, paths_info))
    );

    // Rename chain
    if result.renames_followed && result.rename_chain.len() > 1 {
        let chain_str: Vec<&str> = result.rename_chain.iter().map(String::as_str).collect();
        println!(
            "  Rename chain: {}",
            Theme::dim().render(&chain_str.join(" -> "))
        );
    }

    if result.merge_requests.is_empty() {
        println!(
            "\n  {} {}",
            Icons::info(),
            Theme::dim().render("No merge requests found touching this file.")
        );
        if !result.renames_followed && result.rename_chain.len() == 1 {
            println!(
                "  {} Searched: {}",
                Icons::info(),
                Theme::dim().render(&result.rename_chain[0])
            );
        }
        for hint in &result.hints {
            println!("  {} {}", Icons::info(), Theme::dim().render(hint));
        }
        println!();
        return;
    }

    println!();

    for mr in &result.merge_requests {
        let (icon, state_style) = match mr.state.as_str() {
            "merged" => (Icons::mr_merged(), Theme::accent()),
            "opened" => (Icons::mr_opened(), Theme::success()),
            "closed" => (Icons::mr_closed(), Theme::warning()),
            _ => (Icons::mr_opened(), Theme::dim()),
        };

        let date = mr
            .merged_at_iso
            .as_deref()
            .or(Some(mr.updated_at_iso.as_str()))
            .unwrap_or("")
            .split('T')
            .next()
            .unwrap_or("");

        println!(
            "  {} {}  {}  {}  @{}  {}  {}",
            icon,
            Theme::accent().render(&format!("!{}", mr.iid)),
            render::truncate(&mr.title, render::flex_width(45, 20)),
            state_style.render(&mr.state),
            mr.author_username,
            date,
            Theme::dim().render(&mr.change_type),
        );
    }

    // Discussions
    if !result.discussions.is_empty() {
        println!(
            "\n  {} File discussions ({}):",
            Icons::note(),
            result.discussions.len()
        );
        for d in &result.discussions {
            let date = d.created_at_iso.split('T').next().unwrap_or("");
            println!(
                "    @{} ({}) [{}]: {}",
                d.author_username,
                date,
                Theme::dim().render(&d.path),
                d.body_snippet
            );
        }
    }

    println!();
}

// ── Robot (JSON) output ─────────────────────────────────────────────────────

pub fn print_file_history_json(result: &FileHistoryResult, elapsed_ms: u64) -> Result<()> {
    let output = serde_json::json!({
        "ok": true,
        "data": {
            "path": result.path,
            "rename_chain": if result.renames_followed { Some(&result.rename_chain) } else { None },
            "merge_requests": result.merge_requests,
            "discussions": if result.discussions.is_empty() { None } else { Some(&result.discussions) },
        },
        "meta": {
            "elapsed_ms": elapsed_ms,
            "total_mrs": result.total_mrs,
            "renames_followed": result.renames_followed,
            "paths_searched": result.paths_searched,
            "hints": if result.hints.is_empty() { None } else { Some(&result.hints) },
        }
    });

    println!(
        "{}",
        serde_json::to_string(&output)
            .map_err(|e| LoreError::Other(format!("JSON serialization failed: {e}")))?
    );
    Ok(())
}
