use std::collections::HashMap;
use std::sync::Arc;

use hamelin_lib::catalog::{Catalog, CatalogProvider, Column};
use hamelin_lib::err::{ContextualTranslationErrors, WithTranslationErrors};
use hamelin_lib::provider::EnvironmentProvider;
use hamelin_lib::tree::ast::query::Query;
use hamelin_lib::tree::ast::ParseWithErrors;
use hamelin_lib::tree::options::{TemplateParameterKind, TypeCheckOptions};
use hamelin_lib::tree::typed_ast::query::TypedStatement;
use hamelin_lib::type_check_with_options;
use serde::Serialize;
use tsify::Tsify;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;

use crate::wasm_template_params::parse_template_parameters_js;

/// Parse the query, run typecheck with the provided options, and fold all parse and semantic
/// errors into a single [`ContextualTranslationErrors`]. Returns the typed statement only when
/// no errors were produced. Shared by every WASM entry point that needs a typed query so they
/// stay in lockstep when error folding rules change.
pub fn typecheck_query(
    query: String,
    options: TypeCheckOptions,
) -> Result<TypedStatement, ContextualTranslationErrors> {
    let (ast, parse_errors) = Query::parse_with_errors(query.clone());
    let WithTranslationErrors {
        output: typed_statement,
        errors: semantic_errors,
    } = type_check_with_options::<Query>(Arc::new(ast), options);
    let mut errors = parse_errors;
    errors.extend(semantic_errors);
    if !errors.is_empty() {
        return Err(ContextualTranslationErrors::new(query, errors));
    }
    Ok(typed_statement)
}

#[derive(Serialize, Tsify)]
#[tsify(into_wasm_abi)]
pub struct CompileHamelinQueryResultOk {
    pub columns: Vec<Column>,
}

#[derive(Serialize, Tsify)]
#[tsify(into_wasm_abi)]
pub enum CompileHamelinQueryResult {
    Ok(CompileHamelinQueryResultOk),
    Err(ContextualTranslationErrors),
}

#[wasm_bindgen]
pub fn compile_query(
    query: String,
    catalog: Catalog,
    template_parameters: Option<JsValue>,
) -> Result<CompileHamelinQueryResult, JsValue> {
    let catalog_provider = Arc::new(
        CatalogProvider::try_from(catalog)
            .map_err(|e| JsValue::from_str(e.to_string().as_str()))?,
    );
    let tmpl = parse_template_parameters_js(template_parameters)?;
    Ok(compile_query_with_provider(query, catalog_provider, tmpl))
}

pub fn compile_query_with_provider(
    query: String,
    provider: Arc<dyn EnvironmentProvider>,
    template_parameters: Option<Arc<HashMap<String, TemplateParameterKind>>>,
) -> CompileHamelinQueryResult {
    let opts = TypeCheckOptions::builder()
        .provider(provider)
        .maybe_template_parameters(template_parameters)
        .build();
    match typecheck_query(query, opts) {
        Ok(typed_statement) => CompileHamelinQueryResult::Ok(CompileHamelinQueryResultOk {
            columns: typed_statement.schema().into(),
        }),
        Err(errors) => CompileHamelinQueryResult::Err(errors),
    }
}

#[derive(Serialize, Tsify)]
#[tsify(into_wasm_abi)]
pub enum QueryDatasetsResult {
    Ok(Vec<String>),
    Err(ContextualTranslationErrors),
}

/// Bad `template_parameters` JSON is reported as a `JsValue` exception (matching
/// [`compile_query`]); query parse / dataset-resolution errors are reported via
/// [`QueryDatasetsResult::Err`] so they can be rendered against the source.
#[wasm_bindgen]
pub fn get_statement_datasets(
    query: String,
    template_parameters: Option<JsValue>,
) -> Result<QueryDatasetsResult, JsValue> {
    let tmpl = parse_template_parameters_js(template_parameters)?;

    let (ast, errors) = Query::parse_with_errors(query.clone());
    if !errors.is_empty() {
        return Ok(QueryDatasetsResult::Err(ContextualTranslationErrors::new(
            query, errors,
        )));
    }

    let datasets_result = match tmpl.as_ref() {
        None => ast.datasets(),
        Some(specs) => ast.datasets_with_template_parameters(specs.as_ref()),
    };

    Ok(match datasets_result {
        Ok(datasets) => {
            QueryDatasetsResult::Ok(datasets.into_iter().map(|id| id.to_string()).collect())
        }
        Err(error) => QueryDatasetsResult::Err(ContextualTranslationErrors::new(
            query,
            (*error).clone().single(),
        )),
    })
}
