use rusqlite::Connection;

use super::error::{LoreError, Result};
use super::file_history::resolve_rename_chain;

// ─── SQL Helpers ─────────────────────────────────────────────────────────────

/// Escape LIKE metacharacters (`%`, `_`, `\`).
/// All queries using this must include `ESCAPE '\'`.
pub fn escape_like(input: &str) -> String {
    input
        .replace('\\', "\\\\")
        .replace('%', "\\%")
        .replace('_', "\\_")
}

/// Normalize user-supplied repo paths to match stored DiffNote / file-change paths.
/// - trims whitespace
/// - strips leading "./" and "/" (repo-relative paths)
/// - converts '\' to '/' when no '/' present (Windows paste)
/// - collapses repeated "//"
pub fn normalize_repo_path(input: &str) -> String {
    let mut s = input.trim().to_string();
    // Windows backslash normalization (only when no forward slashes present)
    if s.contains('\\') && !s.contains('/') {
        s = s.replace('\\', "/");
    }
    // Strip leading ./
    while s.starts_with("./") {
        s = s[2..].to_string();
    }
    // Strip leading /
    s = s.trim_start_matches('/').to_string();
    // Collapse repeated //
    while s.contains("//") {
        s = s.replace("//", "/");
    }
    s
}

// ─── Path Query Resolution ──────────────────────────────────────────────────

/// Describes how to match a user-supplied path in SQL.
#[derive(Debug)]
pub struct PathQuery {
    /// The parameter value to bind.
    pub value: String,
    /// If true: use `LIKE value ESCAPE '\'`. If false: use `= value`.
    pub is_prefix: bool,
}

/// Result of a suffix probe against the DB.
pub enum SuffixResult {
    /// Suffix probe was not attempted (conditions not met).
    NotAttempted,
    /// No paths matched the suffix.
    NoMatch,
    /// Exactly one distinct path matched — auto-resolve.
    Unique(String),
    /// Multiple distinct paths matched — user must disambiguate.
    Ambiguous(Vec<String>),
}

/// Build a path query from a user-supplied path, with project-scoped DB probes.
///
/// Resolution strategy (in priority order):
/// 1. Trailing `/` → directory prefix (LIKE `path/%`)
/// 2. Exact match probe against notes + `mr_file_changes` → exact (= `path`)
/// 3. Directory prefix probe → prefix (LIKE `path/%`)
/// 4. Suffix probe for bare filenames → auto-resolve or ambiguity error
/// 5. Heuristic fallback: `.` in last segment → file, else → directory prefix
pub fn build_path_query(
    conn: &Connection,
    path: &str,
    project_id: Option<i64>,
) -> Result<PathQuery> {
    let trimmed = path.trim_end_matches('/');
    let last_segment = trimmed.rsplit('/').next().unwrap_or(trimmed);
    let is_root = !trimmed.contains('/');
    let forced_dir = path.ends_with('/');
    // Heuristic is now only a fallback; probes decide first when ambiguous.
    let looks_like_file = !forced_dir && (is_root || last_segment.contains('.'));

    // Probe 1: exact file exists in DiffNotes OR mr_file_changes (project-scoped)
    // Checks both new_path and old_path to support querying renamed files.
    let exact_exists = conn
        .query_row(
            "SELECT 1 FROM notes INDEXED BY idx_notes_diffnote_path_created
         WHERE note_type = 'DiffNote'
           AND is_system = 0
           AND (position_new_path = ?1 OR position_old_path = ?1)
           AND (?2 IS NULL OR project_id = ?2)
         LIMIT 1",
            rusqlite::params![trimmed, project_id],
            |_| Ok(()),
        )
        .is_ok()
        || conn
            .query_row(
                "SELECT 1 FROM mr_file_changes
             WHERE (new_path = ?1 OR old_path = ?1)
               AND (?2 IS NULL OR project_id = ?2)
             LIMIT 1",
                rusqlite::params![trimmed, project_id],
                |_| Ok(()),
            )
            .is_ok();

    // Probe 2: directory prefix exists in DiffNotes OR mr_file_changes (project-scoped)
    let prefix_exists = if !forced_dir && !exact_exists {
        let escaped = escape_like(trimmed);
        let pat = format!("{escaped}/%");
        conn.query_row(
            "SELECT 1 FROM notes INDEXED BY idx_notes_diffnote_path_created
             WHERE note_type = 'DiffNote'
               AND is_system = 0
               AND (position_new_path LIKE ?1 ESCAPE '\\' OR position_old_path LIKE ?1 ESCAPE '\\')
               AND (?2 IS NULL OR project_id = ?2)
             LIMIT 1",
            rusqlite::params![pat, project_id],
            |_| Ok(()),
        )
        .is_ok()
            || conn
                .query_row(
                    "SELECT 1 FROM mr_file_changes
                 WHERE (new_path LIKE ?1 ESCAPE '\\' OR old_path LIKE ?1 ESCAPE '\\')
                   AND (?2 IS NULL OR project_id = ?2)
                 LIMIT 1",
                    rusqlite::params![pat, project_id],
                    |_| Ok(()),
                )
                .is_ok()
    } else {
        false
    };

    // Probe 3: suffix match — user typed a bare filename or partial path that
    // doesn't exist as-is. Search for full paths ending with /input (or equal to input).
    // This handles "login.rs" matching "src/auth/login.rs".
    let suffix_resolved = if !forced_dir && !exact_exists && !prefix_exists && looks_like_file {
        suffix_probe(conn, trimmed, project_id)?
    } else {
        SuffixResult::NotAttempted
    };

    match suffix_resolved {
        SuffixResult::Unique(full_path) => Ok(PathQuery {
            value: full_path,
            is_prefix: false,
        }),
        SuffixResult::Ambiguous(candidates) => {
            // Check if all candidates are the same file connected by renames.
            // resolve_rename_chain requires a concrete project_id.
            if let Some(pid) = project_id
                && let Some(resolved) = try_resolve_rename_ambiguity(conn, pid, &candidates)?
            {
                return Ok(PathQuery {
                    value: resolved,
                    is_prefix: false,
                });
            }
            let list = candidates
                .iter()
                .map(|p| format!("  {p}"))
                .collect::<Vec<_>>()
                .join("\n");
            Err(LoreError::Ambiguous(format!(
                "'{trimmed}' matches multiple paths. Use the full path or -p to scope:\n{list}"
            )))
        }
        SuffixResult::NotAttempted | SuffixResult::NoMatch => {
            // Original logic: exact > prefix > heuristic
            let is_file = if forced_dir {
                false
            } else if exact_exists {
                true
            } else if prefix_exists {
                false
            } else {
                looks_like_file
            };

            if is_file {
                Ok(PathQuery {
                    value: trimmed.to_string(),
                    is_prefix: false,
                })
            } else {
                let escaped = escape_like(trimmed);
                Ok(PathQuery {
                    value: format!("{escaped}/%"),
                    is_prefix: true,
                })
            }
        }
    }
}

/// Probe both notes and mr_file_changes for paths ending with the given suffix.
/// Searches both new_path and old_path columns to support renamed file resolution.
/// Returns up to 11 distinct candidates (enough to detect ambiguity + show a useful list).
pub fn suffix_probe(
    conn: &Connection,
    suffix: &str,
    project_id: Option<i64>,
) -> Result<SuffixResult> {
    let escaped = escape_like(suffix);
    let suffix_pat = format!("%/{escaped}");

    let mut stmt = conn.prepare_cached(
        "SELECT DISTINCT full_path FROM (
            SELECT position_new_path AS full_path
            FROM notes INDEXED BY idx_notes_diffnote_path_created
            WHERE note_type = 'DiffNote'
              AND is_system = 0
              AND (position_new_path LIKE ?1 ESCAPE '\\' OR position_new_path = ?2)
              AND (?3 IS NULL OR project_id = ?3)
            UNION
            SELECT new_path AS full_path FROM mr_file_changes
            WHERE (new_path LIKE ?1 ESCAPE '\\' OR new_path = ?2)
              AND (?3 IS NULL OR project_id = ?3)
            UNION
            SELECT position_old_path AS full_path FROM notes
            WHERE note_type = 'DiffNote'
              AND is_system = 0
              AND position_old_path IS NOT NULL
              AND (position_old_path LIKE ?1 ESCAPE '\\' OR position_old_path = ?2)
              AND (?3 IS NULL OR project_id = ?3)
            UNION
            SELECT old_path AS full_path FROM mr_file_changes
            WHERE old_path IS NOT NULL
              AND (old_path LIKE ?1 ESCAPE '\\' OR old_path = ?2)
              AND (?3 IS NULL OR project_id = ?3)
        )
        ORDER BY full_path
        LIMIT 11",
    )?;

    let candidates: Vec<String> = stmt
        .query_map(rusqlite::params![suffix_pat, suffix, project_id], |row| {
            row.get(0)
        })?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    match candidates.len() {
        0 => Ok(SuffixResult::NoMatch),
        1 => Ok(SuffixResult::Unique(candidates.into_iter().next().unwrap())),
        _ => Ok(SuffixResult::Ambiguous(candidates)),
    }
}

/// Maximum rename hops when resolving ambiguity.
const AMBIGUITY_MAX_RENAME_HOPS: usize = 10;

/// When suffix probe returns multiple paths, check if they are all the same file
/// connected by renames. If so, return the "newest" path (the leaf of the chain
/// that is never renamed away from). Returns `None` if truly ambiguous.
fn try_resolve_rename_ambiguity(
    conn: &Connection,
    project_id: i64,
    candidates: &[String],
) -> Result<Option<String>> {
    // BFS from the first candidate to discover the full rename chain.
    let chain = resolve_rename_chain(conn, project_id, &candidates[0], AMBIGUITY_MAX_RENAME_HOPS)?;

    // If any candidate is NOT in the chain, these are genuinely different files.
    if !candidates.iter().all(|c| chain.contains(c)) {
        return Ok(None);
    }

    // All candidates are the same file. Find the "newest" path: the one that
    // appears as new_path in a rename but is never old_path in a subsequent rename
    // (within the chain). This is the leaf of the rename DAG.
    let placeholders: Vec<String> = (0..chain.len()).map(|i| format!("?{}", i + 2)).collect();
    let in_clause = placeholders.join(", ");

    // Find paths that are old_path in a rename where new_path is also in the chain.
    let sql = format!(
        "SELECT DISTINCT old_path FROM mr_file_changes \
         WHERE project_id = ?1 \
           AND change_type = 'renamed' \
           AND old_path IN ({in_clause}) \
           AND new_path IN ({in_clause})"
    );

    let mut stmt = conn.prepare(&sql)?;
    let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
    params.push(Box::new(project_id));
    for p in &chain {
        params.push(Box::new(p.clone()));
    }
    let param_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect();

    let old_paths: Vec<String> = stmt
        .query_map(param_refs.as_slice(), |row| row.get(0))?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    // The newest path is a candidate that is NOT an old_path in any intra-chain rename.
    let newest = candidates.iter().find(|c| !old_paths.contains(c));

    Ok(newest.cloned().or_else(|| Some(candidates[0].clone())))
}

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