use serde::Serialize;
use tracing::info;

use super::error::Result;
use super::file_history::resolve_rename_chain;
use super::time::ms_to_iso;

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

/// A linked issue found via entity_references on the MR.
#[derive(Debug, Serialize)]
pub struct TraceIssue {
    pub iid: i64,
    pub title: String,
    pub state: String,
    pub reference_type: String,
    pub web_url: Option<String>,
}

/// A DiffNote discussion relevant to the traced file.
#[derive(Debug, Serialize)]
pub struct TraceDiscussion {
    pub discussion_id: String,
    pub mr_iid: i64,
    pub author_username: String,
    pub body: String,
    pub path: String,
    pub created_at_iso: String,
}

/// A single trace chain: an MR that touched the file, plus linked issues and discussions.
#[derive(Debug, Serialize)]
pub struct TraceChain {
    pub mr_iid: i64,
    pub mr_title: String,
    pub mr_state: String,
    pub mr_author: String,
    pub change_type: String,
    pub merged_at_iso: Option<String>,
    pub updated_at_iso: String,
    pub web_url: Option<String>,
    pub issues: Vec<TraceIssue>,
    pub discussions: Vec<TraceDiscussion>,
}

/// Result of a trace query.
#[derive(Debug, Serialize)]
pub struct TraceResult {
    pub path: String,
    pub resolved_paths: Vec<String>,
    pub renames_followed: bool,
    pub trace_chains: Vec<TraceChain>,
    pub total_chains: usize,
    /// Diagnostic hints explaining why results may be empty.
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub hints: Vec<String>,
}

/// Run the trace query: file -> MR -> issue chain.
pub fn run_trace(
    conn: &rusqlite::Connection,
    project_id: Option<i64>,
    path: &str,
    follow_renames: bool,
    include_discussions: bool,
    limit: usize,
) -> Result<TraceResult> {
    // Resolve rename chain
    let (all_paths, renames_followed) = if follow_renames {
        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 {
            (vec![path.to_string()], false)
        }
    } else {
        (vec![path.to_string()], false)
    };

    info!(
        paths = all_paths.len(),
        renames_followed,
        "trace: resolved {} path(s) for '{}'",
        all_paths.len(),
        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 project_filter = if project_id.is_some() {
        "AND mfc.project_id = ?1"
    } else {
        ""
    };

    // Step 1: Find MRs that touched the file
    let mr_sql = format!(
        "SELECT DISTINCT \
            mr.id, mr.iid, mr.title, mr.state, mr.author_username, \
            mfc.change_type, mr.merged_at, mr.updated_at, 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} \
         ORDER BY COALESCE(mr.merged_at, mr.updated_at) DESC \
         LIMIT ?{}",
        all_paths.len() + 2
    );

    let mut stmt = conn.prepare_cached(&mr_sql)?;

    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();

    struct MrRow {
        id: i64,
        iid: i64,
        title: String,
        state: String,
        author: String,
        change_type: String,
        merged_at: Option<i64>,
        updated_at: i64,
        web_url: Option<String>,
    }

    let mr_rows: Vec<MrRow> = stmt
        .query_map(param_refs.as_slice(), |row| {
            Ok(MrRow {
                id: row.get(0)?,
                iid: row.get(1)?,
                title: row.get(2)?,
                state: row.get(3)?,
                author: row.get(4)?,
                change_type: row.get(5)?,
                merged_at: row.get(6)?,
                updated_at: row.get(7)?,
                web_url: row.get(8)?,
            })
        })?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    info!(
        mr_count = mr_rows.len(),
        "trace: found {} MR(s) touching '{}'",
        mr_rows.len(),
        path
    );

    // Step 2: For each MR, find linked issues + optional discussions
    let mut trace_chains = Vec::with_capacity(mr_rows.len());

    for mr in &mr_rows {
        let issues = fetch_linked_issues(conn, mr.id)?;

        let discussions = if include_discussions {
            fetch_trace_discussions(conn, mr.id, mr.iid, &all_paths)?
        } else {
            Vec::new()
        };

        info!(
            mr_iid = mr.iid,
            issues = issues.len(),
            discussions = discussions.len(),
            "trace: MR !{}: {} issue(s), {} discussion(s)",
            mr.iid,
            issues.len(),
            discussions.len()
        );

        trace_chains.push(TraceChain {
            mr_iid: mr.iid,
            mr_title: mr.title.clone(),
            mr_state: mr.state.clone(),
            mr_author: mr.author.clone(),
            change_type: mr.change_type.clone(),
            merged_at_iso: mr.merged_at.map(ms_to_iso),
            updated_at_iso: ms_to_iso(mr.updated_at),
            web_url: mr.web_url.clone(),
            issues,
            discussions,
        });
    }

    let total_chains = trace_chains.len();

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

    Ok(TraceResult {
        path: path.to_string(),
        resolved_paths: all_paths,
        renames_followed,
        trace_chains,
        total_chains,
        hints,
    })
}

/// Fetch issues linked to an MR via entity_references.
/// source = merge_request -> target = issue (closes/mentioned/related)
fn fetch_linked_issues(conn: &rusqlite::Connection, mr_id: i64) -> Result<Vec<TraceIssue>> {
    let sql = "SELECT DISTINCT i.iid, i.title, i.state, er.reference_type, i.web_url \
               FROM entity_references er \
               JOIN issues i ON i.id = er.target_entity_id \
               WHERE er.source_entity_type = 'merge_request' \
                 AND er.source_entity_id = ?1 \
                 AND er.target_entity_type = 'issue' \
                 AND er.target_entity_id IS NOT NULL \
               ORDER BY \
                 CASE er.reference_type WHEN 'closes' THEN 0 WHEN 'related' THEN 1 ELSE 2 END, \
                 i.iid";

    let mut stmt = conn.prepare_cached(sql)?;
    let issues: Vec<TraceIssue> = stmt
        .query_map(rusqlite::params![mr_id], |row| {
            Ok(TraceIssue {
                iid: row.get(0)?,
                title: row.get(1)?,
                state: row.get(2)?,
                reference_type: row.get(3)?,
                web_url: row.get(4)?,
            })
        })?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    Ok(issues)
}

/// Fetch DiffNote discussions on a specific MR that reference the traced paths.
fn fetch_trace_discussions(
    conn: &rusqlite::Connection,
    mr_id: i64,
    mr_iid: i64,
    paths: &[String],
) -> Result<Vec<TraceDiscussion>> {
    let placeholders: Vec<String> = (0..paths.len()).map(|i| format!("?{}", i + 2)).collect();
    let in_clause = placeholders.join(", ");

    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 d.merge_request_id = ?1 \
           AND n.position_new_path IN ({in_clause}) \
           AND n.is_system = 0 \
         ORDER BY n.created_at DESC"
    );

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

    let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
    params.push(Box::new(mr_id));
    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<TraceDiscussion> = stmt
        .query_map(param_refs.as_slice(), |row| {
            let created_at: i64 = row.get(4)?;
            Ok(TraceDiscussion {
                discussion_id: row.get(0)?,
                mr_iid,
                author_username: row.get(1)?,
                body: row.get(2)?,
                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 trace query returned no results.
fn build_trace_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)
}

#[cfg(test)]
#[path = "trace_tests.rs"]
mod tests;
