use camino::{Utf8Path, Utf8PathBuf};
use rayon::prelude::*;
use scip::types::Index;
use thiserror::Error;

use crate::{
    config::Config,
    core::{ddl::walk_ddl, resolver::walk_references, schema::SchemaModel},
    emit::{emit_index, read_file, write_index},
    errors::{EmitError, ParseError, WalkError},
    parse::{ParseFailure, split_and_parse},
    walker::collect_sql_files,
};

type FileEntry = (
    Utf8PathBuf,
    String,
    Vec<crate::core::resolver::ResolvedRef>,
    Vec<ParseFailure>,
);

/// pipeline errors.
#[derive(Debug, Error)]
pub enum PipelineError {
    /// Schema file could not be read or parsed.
    #[error("schema error: {0}")]
    Schema(#[from] ParseError),
    /// Emit or write failed.
    #[error("emit error: {0}")]
    Emit(#[from] EmitError),
    /// File walk failed.
    #[error("walk error: {0}")]
    Walk(#[from] WalkError),
    /// A path contained non-UTF-8 bytes.
    #[error("non-UTF-8 path in config: {0}")]
    NonUtf8Path(std::path::PathBuf),
}

/// Summary statistics returned after a successful run.
#[derive(Debug)]
pub struct RunStats {
    /// Number of SQL files processed (excluding schema).
    pub files_processed: usize,
    /// total SCIP occurrences across all emitted documents.
    pub total_occurrences: usize,
}

/// Run the full indexing pipeline using `config`.
///
/// Reads the schema, walks `root`, processes each `.sql` file in parallel,
/// emits a SCIP index, and writes it to `config.output_path`.
///
/// # Errors
///
/// Returns [`PipelineError`] if any step fails fatally.
pub fn run(config: &Config) -> Result<RunStats, PipelineError> {
    let schema_path = to_utf8(&config.schema_path)?;
    let root_dir = to_utf8(&config.root_dir)?;
    let output_path = to_utf8(&config.output_path)?;

    let (schema_content, schema_failures, model) = load_schema(&schema_path)?;
    tracing::info!(
        tables = model.tables.len(),
        functions = model.functions.len(),
        views = model.views.len(),
        types = model.types.len(),
        triggers = model.triggers.len(),
        policies = model.policies.len(),
        indexes = model.indexes.len(),
        "schema loaded"
    );

    let sql_paths = collect_sql_files(root_dir.as_std_path(), config.no_gitignore)?;
    tracing::info!(count = sql_paths.len(), "sql files discovered");
    let files = process_files(&sql_paths, &model);

    let index = emit_index(
        &schema_path,
        &schema_content,
        &schema_failures,
        &model,
        &files,
        &root_dir,
    );

    let stats = RunStats {
        files_processed: files.len(),
        total_occurrences: count_occurrences(&index),
    };

    write_index(index, &output_path)?;
    Ok(stats)
}

fn to_utf8(path: &std::path::Path) -> Result<Utf8PathBuf, PipelineError> {
    Utf8PathBuf::from_path_buf(path.to_path_buf()).map_err(PipelineError::NonUtf8Path)
}

fn load_schema(
    schema_path: &Utf8Path,
) -> Result<(String, Vec<ParseFailure>, SchemaModel), PipelineError> {
    let content = read_file(schema_path)?;
    let (stmts, failures) = split_and_parse(&content)?;
    let mut model = SchemaModel::default();
    walk_ddl(&stmts, &mut model);
    Ok((content, failures, model))
}

fn process_files(paths: &[Utf8PathBuf], model: &SchemaModel) -> Vec<FileEntry> {
    paths
        .par_iter()
        .filter_map(|path| process_single_file(path, model))
        .collect()
}

fn process_single_file(path: &Utf8PathBuf, model: &SchemaModel) -> Option<FileEntry> {
    let content = match read_file(path) {
        Ok(c) => c,
        Err(e) => {
            tracing::warn!(path = %path, error = %e, "skipping unreadable file");
            return None;
        }
    };
    let (stmts, failures) = match split_and_parse(&content) {
        Ok(pair) => pair,
        Err(e) => {
            tracing::warn!(path = %path, error = %e, "skipping unparseable file");
            return None;
        }
    };
    let refs = walk_references(&stmts, &content, model);
    Some((path.clone(), content, refs, failures))
}

fn count_occurrences(index: &Index) -> usize {
    index.documents.iter().map(|d| d.occurrences.len()).sum()
}
