"""C59 XML document serializer for Liguria ISTAT XML export.

Generates XSD-compliant XML documents from aggregated C59 payload rows.
This module is fully isolated under xml_export/ and does not modify any existing
TXT export, old ISTAT services, or API endpoints.

Uses standard library xml.etree.ElementTree (lxml not required).
"""

from __future__ import annotations

from datetime import date
from typing import Sequence
from xml.etree import ElementTree

from istat.xml_export.exceptions import XmlPayloadValidationError
from istat.xml_export.models.c59_payload import IstatC59RowPayload
from istat.xml_export.xml.xml_utils import format_xml_date


# XSD namespace
C59_NAMESPACE = "http://www.regione.liguria.it/turismo/rimovcli"
NAMESPACE_PREFIX = "rim"

# XML tags
ROOT_TAG = f"{{{C59_NAMESPACE}}}c59"
MENSILE_TAG = f"{{{C59_NAMESPACE}}}mensile"
GIORNALIERO_TAG = f"{{{C59_NAMESPACE}}}giornaliero"
RIGAC59_TAG = f"{{{C59_NAMESPACE}}}rigac59"

# Default software name
DEFAULT_SOFTWARE_NAME = "Aimantis"


def build_c59_xml_document(
    *,
    structure_code: str,
    report_date: date,
    rows: Sequence[IstatC59RowPayload],
    available_rooms: int | None = None,
    available_beds: int | None = None,
    occupied_rooms: int | None = None,
    software_name: str | None = None,
) -> str:
    """Build a complete XSD-compliant C59 XML document.
    
    Args:
        structure_code: Structure identifier (idstruttura)
        report_date: Report date (data) in YYYY-MM-DD format
        rows: Aggregated C59 payload rows (already sorted)
        available_rooms: Number of available rooms (numcameredisp)
        available_beds: Number of available beds (numlettidisp)
        occupied_rooms: Number of occupied rooms (numcamereoccupate)
        software_name: Software name (softwaregestionale), defaults to "Aimantis"
        
    Returns:
        Pretty-printed XML string with UTF-8 encoding and XML declaration
        
    Raises:
        XmlPayloadValidationError: If input data is invalid
    """
    # Validate inputs
    _validate_inputs(structure_code, report_date, rows)
    
    # Build XML tree
    root = _build_root_element(structure_code, report_date)
    
    # Add mensile node (monthly summary)
    _add_mensile_node(
        root,
        available_rooms=available_rooms,
        available_beds=available_beds,
        software_name=software_name,
    )
    
    # Add giornaliero node (daily movement)
    giornaliero = _add_giornaliero_node(root, occupied_rooms=occupied_rooms)
    
    # Add rigac59 nodes (aggregated rows)
    for row in rows:
        _add_rigac59_node(giornaliero, row)
    
    # Serialize to pretty XML string
    return _to_pretty_xml(root)


def _validate_inputs(
    structure_code: str,
    report_date: date,
    rows: Sequence[IstatC59RowPayload],
) -> None:
    """Validate serializer inputs before building XML.
    
    Raises:
        XmlPayloadValidationError: If any input is invalid
    """
    # Validate structure_code
    if not structure_code or not str(structure_code).strip():
        raise XmlPayloadValidationError("structure_code is required and cannot be empty")
    
    # Validate report_date
    if report_date is None:
        raise XmlPayloadValidationError("report_date is required")
    if not isinstance(report_date, date):
        raise XmlPayloadValidationError("report_date must be a date instance")
    
    # Validate rows
    if rows is None:
        raise XmlPayloadValidationError("rows cannot be None (use empty list [])")
    
    for idx, row in enumerate(rows):
        _validate_row(row, idx)


def _validate_row(row: IstatC59RowPayload, index: int) -> None:
    """Validate a single C59 row payload.
    
    Raises:
        XmlPayloadValidationError: If row data is invalid
    """
    if not isinstance(row, IstatC59RowPayload):
        raise XmlPayloadValidationError(
            f"Row {index} must be an IstatC59RowPayload instance"
        )
    
    # Validate nazione
    if row.nazione not in ("i", "e"):
        raise XmlPayloadValidationError(
            f"Row {index}: nazione must be 'i' or 'e', got '{row.nazione}'"
        )
    
    # Validate residenza (must be 3 digits)
    if not row.residenza or not row.residenza.isdigit() or len(row.residenza) != 3:
        raise XmlPayloadValidationError(
            f"Row {index}: residenza must be exactly 3 digits, got '{row.residenza}'"
        )
    
    # Validate numeric counts (must be non-negative)
    if row.arrivi < 0:
        raise XmlPayloadValidationError(
            f"Row {index}: arrivi must be non-negative, got {row.arrivi}"
        )
    if row.partenze < 0:
        raise XmlPayloadValidationError(
            f"Row {index}: partenze must be non-negative, got {row.partenze}"
        )
    if row.presenze < 0:
        raise XmlPayloadValidationError(
            f"Row {index}: presenze must be non-negative, got {row.presenze}"
        )
    if row.diurni < 0:
        raise XmlPayloadValidationError(
            f"Row {index}: diurni must be non-negative, got {row.diurni}"
        )


def _build_root_element(structure_code: str, report_date: date) -> ElementTree.Element:
    """Build the root <c59> element with required attributes.
    
    Args:
        structure_code: Structure identifier
        report_date: Report date
        
    Returns:
        Root XML element
    """
    root = ElementTree.Element(ROOT_TAG)
    root.set("idstruttura", str(structure_code).strip())
    root.set("data", format_xml_date(report_date))
    return root


def _add_mensile_node(
    root: ElementTree.Element,
    *,
    available_rooms: int | None = None,
    available_beds: int | None = None,
    software_name: str | None = None,
) -> ElementTree.Element:
    """Add the <mensile> (monthly summary) node.
    
    Args:
        root: Parent element
        available_rooms: Available rooms count
        available_beds: Available beds count
        software_name: Software name
        
    Returns:
        Mensile XML element
    """
    mensile = ElementTree.SubElement(root, MENSILE_TAG)
    
    # Add attributes if provided
    if available_rooms is not None:
        if available_rooms < 0:
            raise XmlPayloadValidationError(
                "available_rooms must be non-negative"
            )
        mensile.set("numcameredisp", str(available_rooms))
    
    if available_beds is not None:
        if available_beds < 0:
            raise XmlPayloadValidationError(
                "available_beds must be non-negative"
            )
        mensile.set("numlettidisp", str(available_beds))
    
    # Software name (default to "Aimantis")
    software = software_name if software_name else DEFAULT_SOFTWARE_NAME
    mensile.set("softwaregestionale", str(software).strip())
    
    return mensile


def _add_giornaliero_node(
    root: ElementTree.Element,
    *,
    occupied_rooms: int | None = None,
) -> ElementTree.Element:
    """Add the <giornaliero> (daily movement) node.
    
    Args:
        root: Parent element
        occupied_rooms: Occupied rooms count
        
    Returns:
        Giornaliero XML element
    """
    giornaliero = ElementTree.SubElement(root, GIORNALIERO_TAG)
    
    # Add occupied rooms if provided
    if occupied_rooms is not None:
        if occupied_rooms < 0:
            raise XmlPayloadValidationError(
                "occupied_rooms must be non-negative"
            )
        giornaliero.set("numcamereoccupate", str(occupied_rooms))
    
    return giornaliero


def _add_rigac59_node(
    parent: ElementTree.Element,
    row: IstatC59RowPayload,
) -> ElementTree.Element:
    """Add a <rigac59> (movement row) node.
    
    Args:
        parent: Parent element (giornaliero)
        row: C59 row payload
        
    Returns:
        Rigac59 XML element
    """
    riga = ElementTree.SubElement(parent, RIGAC59_TAG)
    
    # Required attributes
    riga.set("nazione", row.nazione)
    riga.set("residenza", row.residenza)
    riga.set("arrivi", str(row.arrivi))
    riga.set("partenze", str(row.partenze))
    riga.set("presenze", str(row.presenze))
    
    # Optional diurni (include if non-zero or explicitly set)
    if row.diurni > 0:
        riga.set("diurni", str(row.diurni))
    else:
        # XSD says diurni is optional, but we include it as "0" for clarity
        riga.set("diurni", "0")
    
    return riga


def _to_pretty_xml(root: ElementTree.Element) -> str:
    """Serialize XML tree to pretty-printed UTF-8 string.
    
    Args:
        root: Root XML element
        
    Returns:
        Pretty-printed XML string with declaration
    """
    # Register namespace prefix to get 'rim' instead of 'ns0'
    ElementTree.register_namespace(NAMESPACE_PREFIX, C59_NAMESPACE)
    
    # Convert to string
    xml_string = ElementTree.tostring(root, encoding="unicode", method="xml")
    
    # Parse back for pretty printing
    import xml.dom.minidom
    
    # Parse and pretty print
    dom = xml.dom.minidom.parseString(xml_string)
    pretty_xml = dom.toprettyxml(indent="    ", encoding=None)
    
    # Remove extra blank lines that minidom adds
    lines = [line for line in pretty_xml.split('\n') if line.strip()]
    pretty_xml = '\n'.join(lines) + '\n'
    
    # Fix XML declaration to include encoding
    if pretty_xml.startswith('<?xml version="1.0"?>'):
        pretty_xml = pretty_xml.replace(
            '<?xml version="1.0"?>',
            '<?xml version="1.0" encoding="utf-8"?>'
        )
    elif pretty_xml.startswith('<?xml version="1.0" ?>'):
        pretty_xml = pretty_xml.replace(
            '<?xml version="1.0" ?>',
            '<?xml version="1.0" encoding="utf-8"?>'
        )
    elif pretty_xml.startswith('<?xml version="1.0" encoding="UTF-8"?>'):
        pretty_xml = pretty_xml.replace(
            '<?xml version="1.0" encoding="UTF-8"?>',
            '<?xml version="1.0" encoding="utf-8"?>'
        )
    
    return pretty_xml
