use crate::{Result, Rudof, errors::ShExError, formats::ShExFormat, types::ShExStatistics};
use rudof_rdf::rdf_core::vocabs::{RdfVocabulary, ShexRVocab};
use rudof_rdf::rdf_core::{BuildRDF, RDFFormat};
use rudof_rdf::rdf_impl::OxigraphInMemory;
use shex_ast::ShExFormatter;
use shex_ast::ShapeLabelIdx;
use shex_ast::ShapeMapParser;
use shex_ast::ir::schema_ir::SchemaIR;
use shex_ast::ir::shape_label::ShapeLabel;
use shex_ast::shapemap::ShapeSelector;
use shex_ast::shexr::shexr_builder::ShExRBuilder;
use std::{io, time::Instant};

pub fn serialize_shex_schema<W: io::Write>(
    rudof: &Rudof,
    shape_label: Option<&str>,
    show_schema: Option<bool>,
    show_statistics: Option<bool>,
    show_dependencies: Option<bool>,
    show_time: Option<bool>,
    show_colors: Option<bool>,
    shex_format: Option<&ShExFormat>,
    writer: &mut W,
) -> Result<()> {
    let timer = Instant::now();

    let (shape_label, show_schema, show_statistics, show_dependencies, show_time, show_colors, shex_format) =
        init_defaults(
            shape_label,
            show_schema,
            show_statistics,
            show_dependencies,
            show_time,
            show_colors,
            shex_format,
        );

    if !shape_label.is_empty() {
        serialize_shape(rudof, &shape_label, writer)?;
    }

    if show_schema {
        serialize_schema(rudof, shex_format, show_colors, writer)?;
    }

    if rudof.config.shex().show_ir() {
        serialize_schema_ir(rudof, writer)?;
    }

    if show_statistics {
        serialize_statistics(rudof, writer)?;
    }

    if show_dependencies {
        serialize_dependencies(rudof, writer)?;
    }

    if show_time {
        writeln!(writer, "elapsed: {:.05?} sec", timer.elapsed().as_secs_f64())
            .map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
    }

    Ok(())
}

fn init_defaults(
    shape_label: Option<&str>,
    show_schema: Option<bool>,
    show_statistics: Option<bool>,
    show_dependencies: Option<bool>,
    show_time: Option<bool>,
    show_colors: Option<bool>,
    shex_format: Option<&ShExFormat>,
) -> (String, bool, bool, bool, bool, bool, ShExFormat) {
    (
        shape_label.map(|s| s.to_string()).unwrap_or_default(),
        show_schema.unwrap_or(true),
        show_statistics.unwrap_or(false),
        show_dependencies.unwrap_or(false),
        show_time.unwrap_or(false),
        show_colors.unwrap_or(false),
        shex_format.copied().unwrap_or_default(),
    )
}

fn get_statistics(rudof: &Rudof) -> Result<ShExStatistics> {
    let shex_schema_ir = rudof.shex_schema_ir.as_ref().ok_or(ShExError::NoShExSchemaLoaded)?;

    Ok(ShExStatistics {
        extends_count: shex_schema_ir.count_extends(),
        local_shapes_count: shex_schema_ir.local_shapes_count(),
        total_shapes_count: shex_schema_ir.total_shapes_count(),
        shapes: shex_schema_ir
            .shapes()
            .map(|(l, s, e)| (l.clone(), s.clone(), e.clone()))
            .collect(),
        dependencies: shex_schema_ir.dependencies(),
        has_imports: !shex_schema_ir.imported_schemas().is_empty(),
        neg_cycles: shex_schema_ir.neg_cycles(),
    })
}

fn serialize_statistics<W: io::Write>(rudof: &Rudof, writer: &mut W) -> Result<()> {
    let stats = get_statistics(rudof)?;

    writeln!(writer, "\n\n\nStatistics:").map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;

    if rudof.config.shex().show_imports() {
        writeln!(
            writer,
            "- Local shapes: {} / Total shapes {}",
            stats.local_shapes_count, stats.total_shapes_count
        )
        .map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
    }

    if rudof.config.shex().show_shapes() {
        writeln!(writer, "- Shapes:",).map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
        for (label, source, _) in &stats.shapes {
            let from_msg = if stats.has_imports {
                format!(" from {source}")
            } else {
                String::new()
            };
            writeln!(writer, "    - {label} {from_msg}")
                .map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
        }
    }

    writeln!(writer, "---end statistics\n").map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;

    Ok(())
}

fn serialize_dependencies<W: io::Write>(rudof: &Rudof, writer: &mut W) -> Result<()> {
    writeln!(writer, "\nDependencies:").map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
    for (source, posneg, target) in &get_statistics(rudof)?.dependencies {
        writeln!(writer, "- {source}-{posneg}->{target}")
            .map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
    }
    writeln!(writer, "---end dependencies\n").map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;

    Ok(())
}

fn serialize_schema_ir<W: io::Write>(rudof: &Rudof, writer: &mut W) -> Result<()> {
    let shex_schema_ir = rudof.shex_schema_ir.as_ref().ok_or(ShExError::NoShExSchemaLoaded)?;
    writeln!(writer, "# ShEx Schema Internal Representation (IR):")
        .map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
    writeln!(writer, "{}", shex_schema_ir).map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
    Ok(())
}

fn serialize_schema(
    rudof: &Rudof,
    shex_format: ShExFormat,
    show_colors: bool,
    writer: &mut impl io::Write,
) -> Result<()> {
    let formatter = match show_colors {
        true => ShExFormatter::default(),
        false => ShExFormatter::default().without_colors(),
    };

    let shex_schema = rudof.shex_schema.as_ref().ok_or(ShExError::NoShExSchemaLoaded)?;

    match shex_format {
        ShExFormat::ShExC => {
            formatter
                .write_schema(shex_schema, writer)
                .map_err(|e| ShExError::FailedSerializingShExSchema {
                    format: shex_format.to_string(),
                    error: e.to_string(),
                })?;
        },
        ShExFormat::ShExJ | ShExFormat::Json | ShExFormat::JsonLd => {
            serde_json::to_writer_pretty(writer, &shex_schema).map_err(|e| ShExError::FailedSerializingShExSchema {
                format: shex_format.to_string(),
                error: e.to_string(),
            })?;
        },
        ShExFormat::Internal => {
            let shex_schema_ir = rudof.shex_schema_ir.as_ref().ok_or(ShExError::NoShExSchemaLoaded)?;
            writeln!(writer, "# ShEx Schema Internal Representation (IR):")
                .map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
            writeln!(writer, "{}", shex_schema_ir)
                .map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
        },
        ShExFormat::Turtle
        | ShExFormat::NTriples
        | ShExFormat::RdfXml
        | ShExFormat::TriG
        | ShExFormat::N3
        | ShExFormat::NQuads => {
            let rdf_format: RDFFormat = shex_format.try_into()?;
            let mut rdf_writer = OxigraphInMemory::new();
            // Carry the schema's own prefixes over into the generated RDF
            // (so e.g. `xsd:` stays `xsd:` instead of expanding to a full
            // IRI everywhere), then add `sx:` for the ShExR vocabulary
            // itself, last so it always wins over a same-named prefix the
            // schema might already declare.
            if let Some(prefixmap) = shex_schema.prefixmap() {
                rdf_writer.merge_prefixes(prefixmap);
            }
            rdf_writer.add_prefix("sx", ShexRVocab::base_iri());
            ShExRBuilder::schema_to_rdf(shex_schema, &mut rdf_writer).map_err(|e| {
                ShExError::FailedSerializingShExSchema {
                    format: shex_format.to_string(),
                    error: e.to_string(),
                }
            })?;
            rdf_writer
                .serialize(&rdf_format, writer)
                .map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
        },
        ShExFormat::Simple => {
            return Err(ShExError::FailedSerializingShExSchema {
                format: shex_format.to_string(),
                error: "this format can be used to read a ShEx schema, but not to write one out; \
                        use shexc, shexj, json, jsonld, internal, turtle, ntriples, rdfxml, trig, n3 or nquads instead"
                    .to_string(),
            }
            .into());
        },
    }

    Ok(())
}

fn serialize_shape<W: io::Write>(rudof: &Rudof, shape_label: &str, writer: &mut W) -> Result<()> {
    let shape_selector = parse_shape_selector(shape_label)?;

    let shex_schema_ir = rudof.shex_schema_ir.as_ref().ok_or(ShExError::NoShExSchemaLoaded)?;

    for shape_expr_label in shape_selector.iter_shape() {
        let shape_label =
            ShapeLabel::from_shape_expr_label(shape_expr_label, &shex_schema_ir.prefixmap()).map_err(|e| {
                ShExError::InvalidShapeLabel {
                    label: shape_expr_label.to_string(),
                    error: format!("{e}"),
                }
            })?;
        if let Some((idx, shape_expr)) = shex_schema_ir.find_label(&shape_label) {
            writeln!(writer, "# Shape {shape_label}")
                .map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
            write!(writer, "  {shape_expr}").map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
            writeln!(writer, "  # Triple expressions with extends:")
                .map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
            show_triple_exprs(idx, shex_schema_ir, writer)?;
            writeln!(writer, "Predicates:").map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
            let preds = shex_schema_ir.get_preds_extends(idx);
            writeln!(
                writer,
                "  # Predicates: [{}]",
                preds.iter().map(|p| p.to_string()).collect::<Vec<_>>().join(" ")
            )
            .map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
        } else {
            write!(writer, "Shape {shape_label} not found in schema")
                .map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
        }
    }
    Ok(())
}

fn show_triple_exprs(idx: &ShapeLabelIdx, schema: &SchemaIR, writer: &mut impl io::Write) -> Result<()> {
    if let Some(triple_exprs) = schema.get_triple_exprs(idx) {
        if let Some(current) = triple_exprs.get(&None) {
            writeln!(
                writer,
                "    Current -> {}",
                current
                    .iter()
                    .map(|t| t.to_string())
                    .collect::<Vec<String>>()
                    .join(", ")
            )
            .map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
        } else {
            writeln!(writer, "    Current -> None?")
                .map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
        }
        if triple_exprs.len() > 1 {
            for (label, exprs) in triple_exprs.iter().filter(|(k, _)| k.is_some()) {
                writeln!(
                    writer,
                    "    {} -> {}",
                    label.as_ref().unwrap(),
                    exprs.iter().map(|t| t.to_string()).collect::<Vec<String>>().join(", ")
                )
                .map_err(|e| ShExError::FailedIoOperation { error: e.to_string() })?;
            }
        }
        Ok(())
    } else {
        Ok(())
    }
}

fn parse_shape_selector(shape_label: &str) -> Result<ShapeSelector> {
    let selector =
        ShapeMapParser::parse_shape_selector(shape_label).map_err(|e| ShExError::ShapeSelectorParseError {
            shape_selector: shape_label.to_string(),
            error: e.to_string(),
        })?;
    Ok(selector)
}
