"""
SVG XML parser.

Parses SVG files and strings into internal document structure.
Uses xml.etree.ElementTree with security settings to prevent external entity attacks.
"""

import io
import logging
import re
import xml.etree.ElementTree as ET
from pathlib import Path as FilePath

from .exceptions import SVGParseError
from .shapes import Circle, Ellipse, Line, Path, PathCommand, Polygon, Polyline, Rectangle
from .transforms import Transform

logger = logging.getLogger(__name__)


def parse_svg(source: str | FilePath | io.IOBase) -> ET.Element:
    """
    Parse SVG content from various sources into XML ElementTree.

    Args:
        source: SVG content as file path, Path object, file-like object, or raw XML string.

    Returns:
        Root XML Element of the SVG document.

    Raises:
        SVGParseError: If XML is malformed or cannot be parsed.
        FileNotFoundError: If source is a file path that doesn't exist.
    """
    # Configure secure parser
    parser = ET.XMLParser()

    try:
        # Determine source type and parse accordingly
        if isinstance(source, (str, FilePath)):
            source_str = str(source)

            # Check if it's a file path or raw XML
            if "<svg" in source_str.lower():
                # Raw XML string
                try:
                    root = ET.fromstring(source_str, parser=parser)
                except ET.ParseError as e:
                    raise SVGParseError(
                        f"Malformed SVG XML: {e}",
                        line=e.position[0] if hasattr(e, "position") else None,
                        column=e.position[1] if hasattr(e, "position") else None,
                    )
            else:
                # File path
                path = FilePath(source_str)
                if not path.exists():
                    raise FileNotFoundError(f"SVG file not found: {path}")

                try:
                    tree = ET.parse(path, parser=parser)
                    root = tree.getroot()
                except ET.ParseError as e:
                    raise SVGParseError(
                        f"Malformed SVG XML in file {path}: {e}",
                        line=e.position[0] if hasattr(e, "position") else None,
                        column=e.position[1] if hasattr(e, "position") else None,
                    )
        else:
            # File-like object
            try:
                tree = ET.parse(source, parser=parser)
                root = tree.getroot()
            except ET.ParseError as e:
                raise SVGParseError(
                    f"Malformed SVG XML: {e}",
                    line=e.position[0] if hasattr(e, "position") else None,
                    column=e.position[1] if hasattr(e, "position") else None,
                )

    except ET.ParseError as e:
        raise SVGParseError(
            f"Failed to parse SVG: {e}",
            line=e.position[0] if hasattr(e, "position") else None,
            column=e.position[1] if hasattr(e, "position") else None,
        )

    # Remove namespace prefixes for easier handling
    _remove_namespaces(root)

    # Verify it's an SVG document
    if root.tag.lower() != "svg":
        raise SVGParseError(f"Root element must be <svg>, found <{root.tag}>")

    logger.debug(f"Parsed SVG document with root tag: {root.tag}")

    return root


def _remove_namespaces(element: ET.Element) -> None:
    """
    Remove XML namespaces from element tags (in-place).

    This simplifies tag comparison by removing namespace prefixes.

    Args:
        element: Element to process recursively.
    """
    # Remove namespace from tag
    if "}" in element.tag:
        element.tag = element.tag.split("}", 1)[1]

    # Process children recursively
    for child in element:
        _remove_namespaces(child)


def parse_shape_from_element(
    element: ET.Element,
) -> Rectangle | Circle | Ellipse | Line | Polyline | Polygon | Path | None:
    """
    Parse a shape from an XML element.

    Args:
        element: XML element representing a shape.

    Returns:
        Shape object, or None if element is not a recognized shape.

    Raises:
        SVGParseError: If shape attributes are invalid.
    """
    tag = element.tag.lower()
    attrs = element.attrib

    try:
        if tag == "rect":
            return _parse_rectangle(attrs)
        elif tag == "circle":
            return _parse_circle(attrs)
        elif tag == "ellipse":
            return _parse_ellipse(attrs)
        elif tag == "line":
            return _parse_line(attrs)
        elif tag == "polyline":
            return _parse_polyline(attrs)
        elif tag == "polygon":
            return _parse_polygon(attrs)
        elif tag == "path":
            return _parse_path(attrs)
        else:
            # Not a shape element
            return None

    except (ValueError, KeyError) as e:
        raise SVGParseError(f"Invalid {tag} attributes: {e}")


def _parse_rectangle(attrs: dict[str, str]) -> Rectangle:
    """Parse <rect> element attributes."""
    x = float(attrs.get("x", 0))
    y = float(attrs.get("y", 0))
    width = float(attrs["width"])
    height = float(attrs["height"])
    rx = float(attrs.get("rx", 0))
    ry = float(attrs.get("ry", 0))

    return Rectangle(x=x, y=y, width=width, height=height, rx=rx, ry=ry)


def _parse_circle(attrs: dict[str, str]) -> Circle:
    """Parse <circle> element attributes."""
    cx = float(attrs.get("cx", 0))
    cy = float(attrs.get("cy", 0))
    r = float(attrs["r"])

    return Circle(cx=cx, cy=cy, r=r)


def _parse_ellipse(attrs: dict[str, str]) -> Ellipse:
    """Parse <ellipse> element attributes."""
    cx = float(attrs.get("cx", 0))
    cy = float(attrs.get("cy", 0))

    # Get rx and ry, handling auto values
    # Per SVG2 spec: if rx/ry is auto or missing, use the other radius
    rx_str = attrs.get("rx", "auto")
    ry_str = attrs.get("ry", "auto")

    # Parse numeric values
    rx = None if rx_str == "auto" else float(rx_str)
    ry = None if ry_str == "auto" else float(ry_str)

    # Handle auto: if one is auto, use the other's value
    if rx is None and ry is None:
        # Both auto/missing - invalid, use 0
        rx = ry = 0.0
    elif rx is None:
        rx = ry if ry is not None else 0.0  # rx is auto, use ry
    elif ry is None:
        ry = rx if rx is not None else 0.0  # ry is auto, use rx

    # Ensure both are float (mypy type check)
    rx_float: float = rx if rx is not None else 0.0
    ry_float: float = ry if ry is not None else 0.0

    return Ellipse(cx=cx, cy=cy, rx=rx_float, ry=ry_float)


def _parse_line(attrs: dict[str, str]) -> Line:
    """Parse <line> element attributes."""
    x1 = float(attrs.get("x1", 0))
    y1 = float(attrs.get("y1", 0))
    x2 = float(attrs.get("x2", 0))
    y2 = float(attrs.get("y2", 0))

    # Parse optional pathLength attribute for dash scaling
    path_length = None
    if "pathLength" in attrs:
        path_length = float(attrs["pathLength"])

    return Line(x1=x1, y1=y1, x2=x2, y2=y2, path_length=path_length)


def _parse_polyline(attrs: dict[str, str]) -> Polyline:
    """Parse <polyline> element attributes."""
    points_str = attrs.get("points", "")
    points = _parse_points_string(points_str)

    return Polyline(points=points)


def _parse_polygon(attrs: dict[str, str]) -> Polygon:
    """Parse <polygon> element attributes."""
    points_str = attrs.get("points", "")
    points = _parse_points_string(points_str)

    return Polygon(points=points)


def _parse_points_string(points_str: str) -> list[tuple[float, float]]:
    """
    Parse points attribute string into list of (x, y) tuples.

    Args:
        points_str: Space or comma-separated list of coordinates.

    Returns:
        List of (x, y) tuples.

    Example:
        >>> _parse_points_string("10,20 30,40 50,60")
        [(10.0, 20.0), (30.0, 40.0), (50.0, 60.0)]
    """
    # Normalize separators (replace commas with spaces)
    normalized = points_str.replace(",", " ")

    # Split into numbers
    numbers = [float(n) for n in normalized.split() if n.strip()]

    # Group into (x, y) pairs
    points = [(numbers[i], numbers[i + 1]) for i in range(0, len(numbers) - 1, 2)]

    return points


def _parse_path(attrs: dict[str, str]) -> Path:
    """Parse <path> element attributes."""
    path_data = attrs.get("d", "")
    commands = parse_path_data(path_data)

    return Path(commands=commands)


def parse_path_data(path_data: str) -> list[PathCommand]:
    """
    Parse SVG path data (d attribute) into list of PathCommand objects.

    Args:
        path_data: Path data string (e.g., "M 10 10 L 20 20 Z").

    Returns:
        List of PathCommand objects.

    Raises:
        ValueError: If path data is malformed.

    Example:
        >>> commands = parse_path_data("M 10 10 L 20 20 Z")
        >>> len(commands)
        3
        >>> commands[0].command_type
        'M'
    """
    if not path_data.strip():
        return []

    commands = []

    # Tokenize path data
    # Match commands (letters) and numbers (including negative and decimals)
    tokens = re.findall(
        r"[MmLlHhVvCcSsQqTtAaZz]|[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?", path_data
    )

    i = 0
    current_command = None

    while i < len(tokens):
        token = tokens[i]

        # Check if token is a command letter
        if token.isalpha():
            current_command = token
            i += 1

            # Collect parameters for this command
            params = []
            while i < len(tokens) and not tokens[i].isalpha():
                try:
                    params.append(float(tokens[i]))
                except ValueError:
                    raise ValueError(f"Invalid number in path data: {tokens[i]}")
                i += 1

            # Create PathCommand objects based on command type
            absolute = current_command.isupper()
            cmd_type = current_command.upper()

            if cmd_type == "Z":
                # Close path - no parameters
                commands.append(PathCommand(cmd_type, absolute, []))
            elif cmd_type in ["M", "L", "T"]:
                # Commands with (x, y) pairs
                for j in range(0, len(params), 2):
                    if j + 1 < len(params):
                        commands.append(PathCommand(cmd_type, absolute, [params[j], params[j + 1]]))
            elif cmd_type in ["H"]:
                # Horizontal line - single x parameter
                for param in params:
                    commands.append(PathCommand(cmd_type, absolute, [param]))
            elif cmd_type in ["V"]:
                # Vertical line - single y parameter
                for param in params:
                    commands.append(PathCommand(cmd_type, absolute, [param]))
            elif cmd_type == "C":
                # Cubic Bezier - 6 parameters (x1, y1, x2, y2, x, y)
                for j in range(0, len(params), 6):
                    if j + 5 < len(params):
                        commands.append(PathCommand(cmd_type, absolute, params[j : j + 6]))
            elif cmd_type == "S":
                # Smooth cubic Bezier - 4 parameters (x2, y2, x, y)
                for j in range(0, len(params), 4):
                    if j + 3 < len(params):
                        commands.append(PathCommand(cmd_type, absolute, params[j : j + 4]))
            elif cmd_type == "Q":
                # Quadratic Bezier - 4 parameters (x1, y1, x, y)
                for j in range(0, len(params), 4):
                    if j + 3 < len(params):
                        commands.append(PathCommand(cmd_type, absolute, params[j : j + 4]))
            elif cmd_type == "A":
                # Arc - 7 parameters (rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y)
                for j in range(0, len(params), 7):
                    if j + 6 < len(params):
                        commands.append(PathCommand(cmd_type, absolute, params[j : j + 7]))
        else:
            # Unexpected token
            raise ValueError(f"Unexpected token in path data: {token}")

    return commands


def parse_transform_attribute(transform_str: str) -> Transform:
    """
    Parse SVG transform attribute into Transform object.

    Args:
        transform_str: Transform attribute value (e.g., "translate(10, 20) rotate(45)").

    Returns:
        Transform object representing the composed transformations.

    Example:
        >>> transform = parse_transform_attribute("translate(10, 20)")
        >>> transform.matrix[0, 2]  # tx
        10.0
    """
    if not transform_str.strip():
        return Transform.identity()

    # Pattern to match transform functions and their parameters
    pattern = r"(\w+)\s*\(([^)]*)\)"
    matches = re.findall(pattern, transform_str)

    result = Transform.identity()

    for func_name, params_str in matches:
        # Parse parameters - SVG allows comma and/or whitespace as separators
        # Examples: "10, 20", "10 20", "10,20", "10, 20 30"
        # Replace commas with spaces, then split by whitespace
        params_str = params_str.replace(",", " ")
        params = [float(p.strip()) for p in params_str.split() if p.strip()]

        # Apply transformation based on function name
        func_name_lower = func_name.lower()

        if func_name_lower == "translate":
            tx = params[0] if len(params) > 0 else 0
            ty = params[1] if len(params) > 1 else 0
            result = result.compose(Transform.translate(tx, ty))

        elif func_name_lower == "rotate":
            angle = params[0] if len(params) > 0 else 0
            cx = params[1] if len(params) > 1 else 0
            cy = params[2] if len(params) > 2 else 0
            result = result.compose(Transform.rotate(angle, cx, cy))

        elif func_name_lower == "scale":
            sx = params[0] if len(params) > 0 else 1
            sy = params[1] if len(params) > 1 else sx
            result = result.compose(Transform.scale(sx, sy))

        elif func_name_lower == "skewx":
            angle = params[0] if len(params) > 0 else 0
            result = result.compose(Transform.skewX(angle))

        elif func_name_lower == "skewy":
            angle = params[0] if len(params) > 0 else 0
            result = result.compose(Transform.skewY(angle))

        elif func_name_lower == "matrix":
            if len(params) >= 6:
                a, b, c, d, e, f = params[:6]
                result = result.compose(Transform.from_matrix(a, b, c, d, e, f))

        else:
            logger.warning(f"Unknown transform function: {func_name}")

    return result
