use std::error::Error;
use std::fmt::{Display, Formatter};
use std::path::Path;

use svg::node::element::path::Data;
use svg::node::element::{Circle, Path as SvgPathElement, Rectangle, Text as SvgText};
use svg::Document;

use crate::model::{Graph, LayoutArtifacts, LayoutConfig, Point, SvgConfig};

const BACKGROUND_COLOR: &str = "#ffffff";
const RING_COLOR: &str = "#d9d9d9";
const EDGE_COLOR: &str = "#5c6773";
const NODE_FILL_COLOR: &str = "#4f81bd";
const NODE_STROKE_COLOR: &str = "#355c8a";
const LABEL_COLOR: &str = "#111111";
const NODE_RADIUS: f64 = 6.0;
const LABEL_FONT_SIZE: usize = 9;
const LABEL_X_OFFSET: f64 = NODE_RADIUS + 4.0;
const LABEL_Y_OFFSET: f64 = NODE_RADIUS + 2.0;
const LABEL_WIDTH_FACTOR: f64 = 0.56;
const EDGE_STROKE_WIDTH: f64 = 1.5;
const RING_STROKE_WIDTH: f64 = 1.0;
const VIEWBOX_HORIZONTAL_MARGIN: f64 = 72.0;
const VIEWBOX_VERTICAL_MARGIN: f64 = 36.0;

#[derive(Debug)]
pub enum SvgExportError {
    Io(std::io::Error),
}

impl Display for SvgExportError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            SvgExportError::Io(error) => write!(f, "failed to write SVG output: {error}"),
        }
    }
}

impl Error for SvgExportError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            SvgExportError::Io(error) => Some(error),
        }
    }
}

impl From<std::io::Error> for SvgExportError {
    fn from(error: std::io::Error) -> Self {
        Self::Io(error)
    }
}

pub fn write_svg_path(
    path: impl AsRef<Path>,
    graph: &Graph,
    artifacts: &LayoutArtifacts,
    layout: LayoutConfig,
) -> Result<(), SvgExportError> {
    write_svg_path_with_options(path, graph, artifacts, layout, SvgConfig::default())
}

pub fn render_svg_string(
    graph: &Graph,
    artifacts: &LayoutArtifacts,
    layout: LayoutConfig,
) -> String {
    render_svg_string_with_options(graph, artifacts, layout, SvgConfig::default())
}

pub fn write_svg_path_with_options(
    path: impl AsRef<Path>,
    graph: &Graph,
    artifacts: &LayoutArtifacts,
    layout: LayoutConfig,
    svg_config: SvgConfig,
) -> Result<(), SvgExportError> {
    svg::save(path, &build_document(graph, artifacts, layout, svg_config)).map_err(Into::into)
}

pub fn render_svg_string_with_options(
    graph: &Graph,
    artifacts: &LayoutArtifacts,
    layout: LayoutConfig,
    svg_config: SvgConfig,
) -> String {
    build_document(graph, artifacts, layout, svg_config).to_string()
}

fn build_document(
    graph: &Graph,
    artifacts: &LayoutArtifacts,
    layout: LayoutConfig,
    svg_config: SvgConfig,
) -> Document {
    let bounds = compute_bounds(graph, artifacts, layout, svg_config);
    let width = (bounds.max_x - bounds.min_x).max(1.0);
    let height = (bounds.max_y - bounds.min_y).max(1.0);

    let mut document = Document::new()
        .set("viewBox", (bounds.min_x, bounds.min_y, width, height))
        .set("width", width)
        .set("height", height);

    document = document.add(
        Rectangle::new()
            .set("x", bounds.min_x)
            .set("y", bounds.min_y)
            .set("width", width)
            .set("height", height)
            .set("fill", BACKGROUND_COLOR),
    );

    for radius in ring_radii(artifacts, layout) {
        document = document.add(
            Circle::new()
                .set("cx", artifacts.center.x)
                .set("cy", artifacts.center.y)
                .set("r", radius)
                .set("fill", "none")
                .set("stroke", RING_COLOR)
                .set("stroke-width", RING_STROKE_WIDTH),
        );
    }

    for data in edge_paths(graph, artifacts, svg_config) {
        document = document.add(
            SvgPathElement::new()
                .set("fill", "none")
                .set("stroke", EDGE_COLOR)
                .set("stroke-width", EDGE_STROKE_WIDTH)
                .set("stroke-linecap", "round")
                .set("stroke-linejoin", "round")
                .set("d", data),
        );
    }

    for node in &graph.nodes {
        document = document.add(
            Circle::new()
                .set("cx", node.x)
                .set("cy", node.y)
                .set("r", NODE_RADIUS)
                .set("fill", NODE_FILL_COLOR)
                .set("stroke", NODE_STROKE_COLOR)
                .set("stroke-width", 1.0),
        );
    }

    if svg_config.show_labels {
        for node in &graph.nodes {
            if let Some(label) = &node.label {
                document = document.add(
                    SvgText::new(label.clone())
                        .set("x", node.x + LABEL_X_OFFSET)
                        .set("y", node.y - LABEL_Y_OFFSET)
                        .set("fill", LABEL_COLOR)
                        .set("font-size", LABEL_FONT_SIZE)
                        .set("font-family", "Arial, Helvetica, sans-serif"),
                );
            }
        }
    }

    document
}

fn edge_paths(graph: &Graph, artifacts: &LayoutArtifacts, svg_config: SvgConfig) -> Vec<Data> {
    if svg_config.shortest_edges {
        graph
            .edges
            .iter()
            .map(|edge| {
                let source = &graph.nodes[edge.source];
                let target = &graph.nodes[edge.target];
                Data::new()
                    .move_to((source.x, source.y))
                    .line_to((target.x, target.y))
            })
            .collect()
    } else {
        artifacts
            .edge_routes
            .iter()
            .filter_map(|route| {
                if route.points.len() < 2 {
                    return None;
                }

                let mut data = Data::new().move_to((route.points[0].x, route.points[0].y));
                for point in route.points.iter().skip(1) {
                    data = data.line_to((point.x, point.y));
                }
                Some(data)
            })
            .collect()
    }
}

fn ring_radii(artifacts: &LayoutArtifacts, layout: LayoutConfig) -> Vec<f64> {
    let Some(max_level) = artifacts.node_levels.iter().copied().max() else {
        return Vec::new();
    };

    let center_only_level = artifacts
        .node_levels
        .iter()
        .filter(|&&level| level == 0)
        .count()
        == 1;
    let start_level = if center_only_level { 1 } else { 0 };

    if start_level > max_level {
        return Vec::new();
    }

    (start_level..=max_level)
        .map(|level| {
            let radial_units = layout.min_radius
                + (level.saturating_sub(start_level) as f64 * layout.level_distance);
            radial_units * layout.node_distance
        })
        .collect()
}

#[derive(Debug, Clone, Copy)]
struct Bounds {
    min_x: f64,
    min_y: f64,
    max_x: f64,
    max_y: f64,
}

impl Bounds {
    fn around(point: Point) -> Self {
        Self {
            min_x: point.x,
            min_y: point.y,
            max_x: point.x,
            max_y: point.y,
        }
    }

    fn include_point(&mut self, point: Point) {
        self.min_x = self.min_x.min(point.x);
        self.min_y = self.min_y.min(point.y);
        self.max_x = self.max_x.max(point.x);
        self.max_y = self.max_y.max(point.y);
    }

    fn include_radius(&mut self, center: Point, radius: f64) {
        self.include_point(Point::new(center.x - radius, center.y - radius));
        self.include_point(Point::new(center.x + radius, center.y + radius));
    }

    fn expand(&mut self, horizontal_margin: f64, vertical_margin: f64) {
        self.min_x -= horizontal_margin;
        self.min_y -= vertical_margin;
        self.max_x += horizontal_margin;
        self.max_y += vertical_margin;
    }
}

fn compute_bounds(
    graph: &Graph,
    artifacts: &LayoutArtifacts,
    layout: LayoutConfig,
    svg_config: SvgConfig,
) -> Bounds {
    let mut bounds = Bounds::around(artifacts.center);

    for radius in ring_radii(artifacts, layout) {
        bounds.include_radius(artifacts.center, radius);
    }

    for node in &graph.nodes {
        bounds.include_radius(Point::new(node.x, node.y), NODE_RADIUS);
        if svg_config.show_labels {
            if let Some(label) = &node.label {
                include_label_bounds(&mut bounds, node, label);
            }
        }
    }

    for route in &artifacts.edge_routes {
        for &point in &route.points {
            bounds.include_point(point);
        }
    }

    bounds.expand(VIEWBOX_HORIZONTAL_MARGIN, VIEWBOX_VERTICAL_MARGIN);
    bounds
}

fn include_label_bounds(bounds: &mut Bounds, node: &crate::model::Node, label: &str) {
    let start_x = node.x + LABEL_X_OFFSET;
    let baseline_y = node.y - LABEL_Y_OFFSET;
    let width = estimate_label_width(label);
    let ascent = LABEL_FONT_SIZE as f64;
    let descent = LABEL_FONT_SIZE as f64 * 0.3;

    bounds.include_point(Point::new(start_x, baseline_y - ascent));
    bounds.include_point(Point::new(start_x + width, baseline_y + descent));
}

fn estimate_label_width(label: &str) -> f64 {
    label.chars().count() as f64 * LABEL_FONT_SIZE as f64 * LABEL_WIDTH_FACTOR
}

#[cfg(test)]
mod tests {
    use std::fs;
    use std::time::{SystemTime, UNIX_EPOCH};

    use super::*;
    use crate::{graph_from_ttl_reader, layout_radial_hierarchy_with_artifacts, Edge, Graph, Node};

    fn simple_graph() -> (Graph, LayoutArtifacts, LayoutConfig) {
        let mut graph = Graph::new(
            vec![
                Node {
                    label: Some("Root".to_owned()),
                    ..Node::default()
                },
                Node {
                    label: Some("Child".to_owned()),
                    ..Node::default()
                },
                Node {
                    label: Some("Leaf".to_owned()),
                    ..Node::default()
                },
            ],
            vec![Edge::new(0, 1), Edge::new(1, 2)],
        );
        let layout = LayoutConfig::default();
        let artifacts = layout_radial_hierarchy_with_artifacts(&mut graph, layout).unwrap();
        (graph, artifacts, layout)
    }

    #[test]
    fn render_svg_contains_svg_root_and_paths() {
        let (graph, artifacts, layout) = simple_graph();
        let svg = render_svg_string(&graph, &artifacts, layout);

        assert!(svg.contains("<svg"));
        assert!(svg.contains("<path"));
    }

    #[test]
    fn render_svg_includes_labels_and_rings() {
        let (graph, artifacts, layout) = simple_graph();
        let svg = render_svg_string(&graph, &artifacts, layout);

        assert!(svg.contains("Root"));
        assert!(svg.contains("Child"));
        assert!(svg.contains("<circle"));
        assert!(svg.contains("font-size=\"9\""));
    }

    #[test]
    fn single_root_layout_skips_level_zero_ring() {
        let (graph, artifacts, layout) = simple_graph();
        let svg = render_svg_string(&graph, &artifacts, layout);
        let ring_count = svg.matches("stroke=\"#d9d9d9\"").count();

        assert_eq!(ring_count, 2);
    }

    #[test]
    fn writes_svg_file_to_disk() {
        let (graph, artifacts, layout) = simple_graph();
        let unique = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let path = std::env::temp_dir().join(format!(
            "radial_sugiyama_svg_test_{}_{}.svg",
            std::process::id(),
            unique
        ));

        write_svg_path(&path, &graph, &artifacts, layout).unwrap();

        let content = fs::read_to_string(&path).unwrap();
        assert!(content.contains("<svg"));
        let _ = fs::remove_file(path);
    }

    #[test]
    fn turtle_to_svg_pipeline_contains_labels() {
        let ttl = "@prefix ex: <http://example.com/> .\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\nex:A rdfs:subClassOf ex:B .\nex:B rdfs:subClassOf ex:C .\n";
        let mut graph = graph_from_ttl_reader(ttl.as_bytes()).unwrap();
        let layout = LayoutConfig::default();
        let artifacts = layout_radial_hierarchy_with_artifacts(&mut graph, layout).unwrap();

        let svg = render_svg_string(&graph, &artifacts, layout);

        assert!(svg.contains("http://example.com/A"));
        assert!(svg.contains("http://example.com/B"));
    }

    #[test]
    fn shortest_edge_option_draws_original_edges_as_direct_segments() {
        let mut graph = Graph::new(
            vec![Node::default(), Node::default(), Node::default()],
            vec![Edge::new(0, 1), Edge::new(1, 2), Edge::new(0, 2)],
        );
        let layout = LayoutConfig::default();
        let artifacts = layout_radial_hierarchy_with_artifacts(&mut graph, layout).unwrap();

        let svg = render_svg_string_with_options(
            &graph,
            &artifacts,
            layout,
            SvgConfig {
                shortest_edges: true,
                show_labels: true,
            },
        );

        assert_eq!(svg.matches("<path").count(), graph.edges.len());
    }

    #[test]
    fn bounds_expand_to_fit_long_labels() {
        let graph = Graph::new(
            vec![Node {
                label: Some("http://example.com/very/long/uri/that/should/fit".to_owned()),
                x: 100.0,
                y: 100.0,
            }],
            vec![],
        );
        let artifacts = LayoutArtifacts {
            node_levels: vec![0],
            edge_offsets: vec![],
            edge_routes: vec![],
            routed_nodes: vec![],
            center: Point::new(100.0, 100.0),
        };
        let bounds = compute_bounds(
            &graph,
            &artifacts,
            LayoutConfig::default(),
            SvgConfig::default(),
        );

        assert!(bounds.max_x > 300.0);
        assert!(bounds.max_y > 100.0);
    }

    #[test]
    fn render_svg_omits_labels_when_disabled() {
        let (graph, artifacts, layout) = simple_graph();
        let svg = render_svg_string_with_options(
            &graph,
            &artifacts,
            layout,
            SvgConfig {
                shortest_edges: false,
                show_labels: false,
            },
        );

        assert!(!svg.contains("Root"));
        assert!(!svg.contains("<text"));
    }
}
