mod relationships;
mod schema;

use camino::Utf8Path;
use scip::types::{
    Diagnostic, Document, Index, Metadata, Occurrence, Severity, SymbolRole, SyntaxKind, ToolInfo,
};

use crate::{
    core::{resolver::ResolvedRef, schema::SchemaModel},
    errors::EmitError,
    parse::ParseFailure,
};

use super::symbol::to_scip_symbol;

pub use schema::build_schema_document;

/// Read file content from disk, mapping IO errors to [`EmitError::Read`].
///
/// # Errors
///
/// Returns [`EmitError::Read`] if the file cannot be opened or read.
pub fn read_file(path: &camino::Utf8Path) -> Result<String, EmitError> {
    std::fs::read_to_string(path.as_std_path()).map_err(|e| EmitError::Read {
        path: path.as_str().to_owned(),
        source: e,
    })
}

// falls back to path unchanged for cross-volume paths
fn relativize(path: &Utf8Path, root: &Utf8Path) -> camino::Utf8PathBuf {
    path.strip_prefix(root)
        .map_or_else(|_| path.to_path_buf(), Utf8Path::to_path_buf)
}

/// Build a complete SCIP index from a populated schema model and resolved refs.
///
/// `schema_path` and each file path in `files` may be absolute; they are
/// relativized against `project_root` before being written into document
/// `relative_path` fields.
#[must_use]
pub fn emit_index(
    schema_path: &Utf8Path,
    schema_content: &str,
    schema_failures: &[ParseFailure],
    model: &SchemaModel,
    files: &[(
        camino::Utf8PathBuf,
        String,
        Vec<ResolvedRef>,
        Vec<ParseFailure>,
    )],
    project_root: &Utf8Path,
) -> Index {
    let mut index = Index::new();
    index.metadata = Some(build_metadata(project_root)).into();

    let rel_schema = relativize(schema_path, project_root);
    let schema_doc = build_schema_document(&rel_schema, schema_content, schema_failures, model);
    index.documents.push(schema_doc);

    for (path, content, refs, failures) in files {
        let rel_path = relativize(path, project_root);
        let doc = build_ref_document(&rel_path, content, refs, failures, model);
        index.documents.push(doc);
    }

    index
}

pub(crate) fn build_ref_document(
    path: &Utf8Path,
    content: &str,
    refs: &[ResolvedRef],
    failures: &[ParseFailure],
    model: &SchemaModel,
) -> Document {
    let line_index = super::line_index::LineIndex::new(content);
    let mut occurrences: Vec<Occurrence> = refs
        .iter()
        .filter_map(|r| {
            let sym = to_scip_symbol(&r.symbol, model);
            if sym.is_empty() {
                tracing::debug!(symbol = r.symbol, "skipping ref with empty SCIP symbol");
                return None;
            }
            let mut occ = Occurrence::new();
            occ.symbol = sym;
            occ.range = line_index.to_scip_range(r.range);
            occ.symbol_roles = 0;
            occ.syntax_kind = SyntaxKind::Identifier.into();
            let enc = line_index.to_scip_range(r.statement_range);
            if enc != occ.range {
                occ.enclosing_range = enc;
            }
            Some(occ)
        })
        .collect();

    for failure in failures {
        occurrences.push(build_failure_occurrence(failure, &line_index));
    }

    let mut doc = Document::new();
    doc.relative_path = path.as_str().into();
    doc.language = "SQL".into();
    doc.occurrences = occurrences;
    doc
}

pub(super) fn build_failure_occurrence(
    failure: &ParseFailure,
    line_index: &super::line_index::LineIndex,
) -> Occurrence {
    let mut occ = Occurrence::new();
    occ.range = line_index.to_scip_range(failure.range);
    occ.syntax_kind = SyntaxKind::UnspecifiedSyntaxKind.into();
    occ.diagnostics = vec![build_parse_diagnostic(&failure.message)];
    occ
}

fn build_parse_diagnostic(message: &str) -> Diagnostic {
    let mut d = Diagnostic::new();
    d.severity = Severity::Error.into();
    message.clone_into(&mut d.message);
    "psql-scip".clone_into(&mut d.source);
    d
}

/// Serialise `index` to `path` using protobuf encoding.
///
/// # Errors
///
/// Returns [`EmitError::Write`] if the file cannot be created or the protobuf
/// serialisation fails.
pub fn write_index(index: Index, path: &Utf8Path) -> Result<(), EmitError> {
    scip::write_message_to_file(path.as_std_path(), index).map_err(|e| EmitError::Write {
        path: path.as_str().to_owned(),
        source: e,
    })
}

fn build_metadata(project_root: &Utf8Path) -> Metadata {
    let mut tool = ToolInfo::new();
    tool.name = "psql-scip".into();
    tool.version = env!("CARGO_PKG_VERSION").into();

    let mut meta = Metadata::new();
    meta.tool_info = Some(tool).into();
    meta.project_root = format!("file://{project_root}");
    meta
}

pub(super) fn def_occurrence(
    sym: &str,
    line_index: &super::line_index::LineIndex,
    range: text_size::TextRange,
) -> Occurrence {
    let mut occ = Occurrence::new();
    sym.clone_into(&mut occ.symbol);
    occ.range = line_index.to_scip_range(range);
    occ.symbol_roles = SymbolRole::Definition as i32;
    occ.syntax_kind = SyntaxKind::IdentifierFunctionDefinition.into();
    occ
}
