//! Walks a style's element tree and turns it into the final SVG markup.
//!
//! The renderer is single-use: it accumulates `<defs>` entries and per-render
//! caches across method calls, so a fresh instance is required per avatar. Any
//! circular color reference encountered during resolution surfaces as an
//! [`Error`] from [`Renderer::render`].

use std::collections::HashSet;
use std::hash::{BuildHasher, Hasher, RandomState};
use std::sync::LazyLock;

use indexmap::IndexMap;
use regex::Regex;

use crate::error::Error;
use crate::prng::fnv1a;
use crate::resolver::Resolver;
use crate::style::{Canvas, Component, DynValue, Element, Style};
use crate::utils::{initials, license, number, xml};

pub struct Renderer<'a> {
    style: &'a Style,
    resolver: &'a Resolver<'a>,
    defs: IndexMap<String, String>,
    cached_seed_hash: Option<String>,
    cached_initials: Option<String>,
}

impl<'a> Renderer<'a> {
    pub fn new(style: &'a Style, resolver: &'a Resolver<'a>) -> Self {
        Self {
            style,
            resolver,
            defs: IndexMap::new(),
            cached_seed_hash: None,
            cached_initials: None,
        }
    }

    /// Builds the complete SVG document for the avatar.
    pub fn render(&mut self) -> Result<String, Error> {
        let style = self.style;
        let resolver = self.resolver;
        let canvas = style.canvas();

        let background = self.render_background(canvas)?;
        let mut body = self.render_elements(canvas.elements())?;

        // Order matters: scale and flip around center, then rotate, translate,
        // and finally clip with border radius (outermost wrapper).
        body = self.apply_scale(body, canvas);
        body = self.apply_flip(body, canvas);
        body = self.apply_rotate(body, canvas);
        body = self.apply_translate(body, canvas);
        body = self.apply_border_radius(format!("{background}{body}"), canvas);

        let metadata = license::xml(style.meta());
        let defs = if self.defs.is_empty() {
            String::new()
        } else {
            format!(
                "<defs>{}</defs>",
                self.defs.values().cloned().collect::<String>()
            )
        };

        let escaped_title = resolver.title().map(|t| xml::escape(&t));

        let mut attrs = vec![
            "xmlns=\"http://www.w3.org/2000/svg\"".to_string(),
            format!(
                "viewBox=\"0 0 {} {}\"",
                number::format(canvas.width()),
                number::format(canvas.height())
            ),
        ];

        let root_attributes = self.render_attributes(style.root_attributes())?;
        if !root_attributes.is_empty() {
            attrs.push(root_attributes.trim_start().to_string());
        }

        match &escaped_title {
            Some(title) => {
                attrs.push("role=\"img\"".to_string());
                attrs.push(format!("aria-label=\"{title}\""));
            }
            None => attrs.push("aria-hidden=\"true\"".to_string()),
        }

        if let Some(size) = resolver.size() {
            let size = number::format(size);
            attrs.push(format!("width=\"{size}\""));
            attrs.push(format!("height=\"{size}\""));
        }

        let title_element = match &escaped_title {
            Some(title) => format!("<title>{title}</title>"),
            None => String::new(),
        };

        let svg = format!(
            "<svg {}>{metadata}{defs}{title_element}{body}</svg>",
            attrs.join(" ")
        );

        Ok(if resolver.id_randomization() {
            randomize_ids(&svg)
        } else {
            svg
        })
    }

    fn apply_flip(&self, body: String, canvas: &Canvas) -> String {
        let flip = self.resolver.flip();

        if flip == "none" {
            return body;
        }

        let w = number::format(canvas.width());
        let h = number::format(canvas.height());

        let transform = match flip.as_str() {
            "horizontal" => format!("translate({w}, 0) scale(-1, 1)"),
            "vertical" => format!("translate(0, {h}) scale(1, -1)"),
            "both" => format!("translate({w}, {h}) scale(-1, -1)"),
            _ => return body,
        };

        format!("<g transform=\"{transform}\">{body}</g>")
    }

    fn apply_scale(&self, body: String, canvas: &Canvas) -> String {
        let scale = self.resolver.scale();

        if scale == 1.0 {
            return body;
        }

        let cx = canvas.width() / 2.0;
        let cy = canvas.height() / 2.0;

        format!(
            "<g transform=\"translate({}, {}) scale({}) translate({}, {})\">{body}</g>",
            number::format(cx),
            number::format(cy),
            number::format(scale),
            number::format(-cx),
            number::format(-cy)
        )
    }

    fn apply_border_radius(&mut self, body: String, canvas: &Canvas) -> String {
        let radius = self.resolver.border_radius();
        let id = format!("clip-{}", self.hash_seed());
        let rx = number::format(radius / 100.0 * canvas.width());
        let ry = number::format(radius / 100.0 * canvas.height());

        self.defs.insert(
            id.clone(),
            format!(
                "<clipPath id=\"{id}\"><rect width=\"{}\" height=\"{}\" rx=\"{rx}\" ry=\"{ry}\"/></clipPath>",
                number::format(canvas.width()),
                number::format(canvas.height())
            ),
        );

        format!("<g clip-path=\"url(#{id})\">{body}</g>")
    }

    fn apply_rotate(&self, body: String, canvas: &Canvas) -> String {
        let rotate = self.resolver.rotate();

        if rotate == 0.0 {
            return body;
        }

        let cx = canvas.width() / 2.0;
        let cy = canvas.height() / 2.0;

        format!(
            "<g transform=\"rotate({}, {}, {})\">{body}</g>",
            number::format(rotate),
            number::format(cx),
            number::format(cy)
        )
    }

    fn apply_translate(&self, body: String, canvas: &Canvas) -> String {
        let tx = self.resolver.translate_x();
        let ty = self.resolver.translate_y();

        if tx == 0.0 && ty == 0.0 {
            return body;
        }

        let x = number::format(tx / 100.0 * canvas.width());
        let y = number::format(ty / 100.0 * canvas.height());

        format!("<g transform=\"translate({x}, {y})\">{body}</g>")
    }

    fn render_background(&mut self, canvas: &Canvas) -> Result<String, Error> {
        if self.resolver.color("background")?.is_empty() {
            return Ok(String::new());
        }

        let fill = xml::escape(&self.resolve_color_reference("background")?);

        Ok(format!(
            "<rect width=\"{}\" height=\"{}\" fill=\"{fill}\"/>",
            number::format(canvas.width()),
            number::format(canvas.height())
        ))
    }

    fn render_elements(&mut self, elements: &[Element]) -> Result<String, Error> {
        let mut out = String::new();

        for element in elements {
            out.push_str(&self.render_element(element)?);
        }

        Ok(out)
    }

    fn render_element(&mut self, element: &Element) -> Result<String, Error> {
        match element.kind() {
            "element" => self.render_svg_element(element),
            "text" => Ok(self.render_text_element(element)),
            "component" => self.render_component_element(element),
            _ => Ok(String::new()),
        }
    }

    fn render_svg_element(&mut self, element: &Element) -> Result<String, Error> {
        let Some(name) = element.name().filter(|n| !n.is_empty()) else {
            return Ok(String::new());
        };

        // The special `defs` name diverts children into the shared <defs> block.
        if name == "defs" {
            for child in element.children() {
                let rendered = self.render_element(child)?;

                if !rendered.is_empty() {
                    let key = match child.attributes().and_then(|a| a.get("id")) {
                        Some(DynValue::Str(id)) => id.clone(),
                        _ => format!("_{}", self.defs.len()),
                    };

                    self.defs.insert(key, rendered);
                }
            }

            return Ok(String::new());
        }

        let attrs = self.render_attributes(element.attributes())?;
        let children = self.render_elements(element.children())?;

        Ok(if children.is_empty() {
            format!("<{name}{attrs}/>")
        } else {
            format!("<{name}{attrs}>{children}</{name}>")
        })
    }

    fn render_text_element(&mut self, element: &Element) -> String {
        match element.value() {
            Some(value) => xml::escape(&self.resolve_value(value)),
            None => String::new(),
        }
    }

    fn render_component_element(&mut self, element: &Element) -> Result<String, Error> {
        let Some(component_name) = element.name() else {
            return Ok(String::new());
        };

        let Some(variant_name) = self.resolver.variant(component_name) else {
            return Ok(String::new());
        };

        let style = self.style;
        let Some(component) = style.components().get(component_name) else {
            return Ok(String::new());
        };
        let Some(variant) = component.variants().get(&variant_name) else {
            return Ok(String::new());
        };

        let id = format!(
            "{}-{}-{}",
            component.source_name(),
            variant_name,
            self.hash_seed()
        );

        if !self.defs.contains_key(&id) {
            let body = self.render_elements(variant.elements())?;
            self.defs
                .insert(id.clone(), format!("<g id=\"{id}\">{body}</g>"));
        }

        let transforms = self.build_transforms(component);
        let user_attributes = element.attributes();

        let merged = if transforms.is_empty() {
            user_attributes.cloned()
        } else {
            let mut merged = user_attributes.cloned().unwrap_or_default();
            let user_transform = match user_attributes.and_then(|a| a.get("transform")) {
                Some(DynValue::Str(t)) if !t.is_empty() => Some(t.clone()),
                _ => None,
            };

            let all_parts: Vec<String> = match user_transform {
                Some(user) => std::iter::once(user).chain(transforms).collect(),
                None => transforms,
            };

            // Insert updates in place if `transform` already exists (keeping its
            // position) or appends it, matching the JS object spread.
            merged.insert("transform".to_string(), DynValue::Str(all_parts.join(" ")));
            Some(merged)
        };

        let attrs = self.render_attributes(merged.as_ref())?;

        Ok(format!("<use{attrs} href=\"#{id}\"/>"))
    }

    /// The per-component `transform` fragments (translate, rotate, scale),
    /// ordered so that — once joined — scale is the innermost transform.
    fn build_transforms(&self, component: &Component) -> Vec<String> {
        let (rotate, translate_x, translate_y, scale) =
            self.resolver.component_transform(component.name());

        if translate_x == 0.0 && translate_y == 0.0 && rotate == 0.0 && scale == 1.0 {
            return Vec::new();
        }

        let mut transforms = Vec::new();
        let cx = component.width() / 2.0;
        let cy = component.height() / 2.0;
        let cx_value = number::format(cx);
        let cy_value = number::format(cy);

        if translate_x != 0.0 || translate_y != 0.0 {
            let x = number::format(translate_x / 100.0 * component.width());
            let y = number::format(translate_y / 100.0 * component.height());
            transforms.push(format!("translate({x}, {y})"));
        }

        if rotate != 0.0 {
            transforms.push(format!(
                "rotate({}, {cx_value}, {cy_value})",
                number::format(rotate)
            ));
        }

        if scale != 1.0 {
            transforms.push(format!(
                "translate({cx_value}, {cy_value}) scale({}) translate({}, {})",
                number::format(scale),
                number::format(-cx),
                number::format(-cy)
            ));
        }

        transforms
    }

    fn render_attributes(
        &mut self,
        attributes: Option<&IndexMap<String, DynValue>>,
    ) -> Result<String, Error> {
        let Some(attributes) = attributes else {
            return Ok(String::new());
        };

        let mut parts: Vec<String> = Vec::new();

        for (key, value) in attributes {
            let resolved = self.resolve_attribute_value(value)?;
            parts.push(format!("{key}=\"{}\"", xml::escape(&resolved)));
        }

        Ok(if parts.is_empty() {
            String::new()
        } else {
            format!(" {}", parts.join(" "))
        })
    }

    fn resolve_attribute_value(&mut self, value: &DynValue) -> Result<String, Error> {
        match value {
            DynValue::Str(s) => Ok(s.clone()),
            DynValue::Ref(reference) if reference.kind() == "color" => {
                self.resolve_color_reference(reference.name())
            }
            DynValue::Ref(reference) => Ok(self.resolve_variable(reference.name())),
        }
    }

    /// Resolves a named color into a hex string (solid / single color) or a
    /// `url(#…)` gradient reference, registering the gradient in `<defs>`.
    fn resolve_color_reference(&mut self, name: &str) -> Result<String, Error> {
        let colors = self.resolver.color(name)?;
        let fill = self.resolver.color_fill(name);

        if fill == "solid" || colors.len() <= 1 {
            return Ok(colors
                .into_iter()
                .next()
                .unwrap_or_else(|| "none".to_string()));
        }

        Ok(self.build_gradient_def(name, &colors, &fill))
    }

    fn build_gradient_def(&mut self, name: &str, colors: &[String], fill: &str) -> String {
        let rotation = self.resolver.color_angle(name);
        let id = format!("{name}-color-{}", self.hash_seed());
        let tag = if fill == "linear" {
            "linearGradient"
        } else {
            "radialGradient"
        };

        let rotate_attr = if rotation == 0.0 {
            String::new()
        } else {
            format!(
                " gradientTransform=\"rotate({}, 0.5, 0.5)\"",
                number::format(rotation)
            )
        };

        let last = colors.len() as f64 - 1.0;
        let mut stops = String::new();
        for (i, color) in colors.iter().enumerate() {
            let offset = number::format((i as f64 / last) * 100.0);
            stops.push_str(&format!(
                "<stop offset=\"{offset}%\" stop-color=\"{}\"/>",
                xml::escape(color)
            ));
        }

        self.defs.insert(
            id.clone(),
            format!("<{tag} id=\"{id}\"{rotate_attr}>{stops}</{tag}>"),
        );

        format!("url(#{id})")
    }

    fn resolve_value(&mut self, value: &DynValue) -> String {
        match value {
            DynValue::Str(s) => s.clone(),
            DynValue::Ref(reference) if reference.kind() == "variable" => {
                self.resolve_variable(reference.name())
            }
            DynValue::Ref(_) => String::new(),
        }
    }

    fn resolve_variable(&mut self, name: &str) -> String {
        match name {
            "initial" => self
                .initials()
                .chars()
                .next()
                .map(String::from)
                .unwrap_or_default(),
            "initials" => self.initials(),
            "fontWeight" => number::format(self.resolver.font_weight()),
            "fontFamily" => self.resolver.font_family(),
            _ => String::new(),
        }
    }

    fn initials(&mut self) -> String {
        self.cached_initials
            .get_or_insert_with(|| initials::from_seed(&self.resolver.seed()))
            .clone()
    }

    fn hash_seed(&mut self) -> String {
        self.cached_seed_hash
            .get_or_insert_with(|| fnv1a::hex(&self.resolver.seed()))
            .clone()
    }
}

/// Suffixes every `id` declaration and reference with a random hex string so
/// that multiple instances of the same avatar do not collide in a shared
/// document. Uses process randomness intentionally — a PRNG-derived suffix
/// would produce the same ID for the same seed.
fn randomize_ids(svg: &str) -> String {
    static ID_DECL: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"\bid="([^"]+)""#).unwrap());

    let random = RandomState::new().build_hasher().finish();
    let suffix = format!("{:06x}", (random as u32) & 0x00ff_ffff);

    let mut seen = HashSet::new();
    let mut ids: Vec<String> = Vec::new();
    for caps in ID_DECL.captures_iter(svg) {
        let id = caps[1].to_string();
        if seen.insert(id.clone()) {
            ids.push(id);
        }
    }

    if ids.is_empty() {
        return svg.to_string();
    }

    let alternation = ids
        .iter()
        .map(|id| regex::escape(id))
        .collect::<Vec<_>>()
        .join("|");
    let pattern = format!(r##"(id="|url\(#|href="#)({alternation})("|\))"##);
    let re = Regex::new(&pattern).unwrap();

    re.replace_all(svg, |caps: &regex::Captures| {
        format!("{}{}-{suffix}{}", &caps[1], &caps[2], &caps[3])
    })
    .into_owned()
}
