"""Parser for the ADO Recordset XML format returned by ado.ExecuteSQL."""

from __future__ import annotations

import xml.etree.ElementTree as ET
from dataclasses import dataclass, field
from datetime import datetime
from typing import overload


def _strip_namespaces(root: ET.Element) -> ET.Element:
    """Strip all XML namespace prefixes from tags and attribute names in-place.

    After this call every tag and attribute key is its bare local name,
    so ``{uuid:...}AttributeType`` becomes ``AttributeType``.

    Args:
        root: The root element of a parsed XML tree.

    Returns:
        The same root element, mutated in-place.
    """
    for elem in root.iter():
        elem.tag = elem.tag.split("}", 1)[1] if "}" in elem.tag else elem.tag
        elem.attrib = {(k.split("}", 1)[1] if "}" in k else k): v for k, v in elem.attrib.items()}
    return root


@dataclass(frozen=True)
class ECMDbColumn:
    """Describes a single column in an ADO query result.

    Attributes:
        name: Column name as returned by the server.
        number: 1-based column index.
        data_type: ADO data type string (e.g. ``"string"``, ``"int"``, ``"dateTime"``).
        max_length: Maximum byte length declared in the schema, or ``None`` if absent.
        nullable: Whether the column allows ``NULL`` values.
        base_table: Source table name, or ``None`` if not provided by the schema.
        base_column: Source column name, or ``None`` if not provided by the schema.
    """

    name: str
    number: int
    data_type: str
    max_length: int | None
    nullable: bool
    base_table: str | None
    base_column: str | None


@dataclass
class ECMDbRow:
    """Represents a single result row from an ADO query.

    Column values are stored as raw strings as returned by the server.
    ``None`` indicates a ``NULL`` value (the attribute was absent on the ``<z:row>`` element).

    Attributes:
        _columns: Ordered list of column definitions matching the schema.
        _values: Mapping of column name to raw string value (``None`` for ``NULL``).
    """

    _columns: list[ECMDbColumn] = field(repr=False)
    _values: dict[str, str | None] = field(repr=False)

    def get(self, name: str) -> str | None:
        """Return the raw string value for *name*, or ``None`` for ``NULL`` / missing columns.

        Args:
            name: Column name (case-sensitive, as returned by the server).

        Returns:
            The raw string value, or ``None``.
        """
        return self._values.get(name)

    @overload
    def typed(self, name: str, type_: type[str]) -> str | None: ...

    @overload
    def typed(self, name: str, type_: type[bool]) -> bool | None: ...

    @overload
    def typed(self, name: str, type_: type[int]) -> int | None: ...

    @overload
    def typed(self, name: str, type_: type[float]) -> float | None: ...

    @overload
    def typed(self, name: str, type_: type[datetime]) -> datetime | None: ...

    def typed(self, name: str, type_: type) -> str | int | float | bool | datetime | None:
        """Return the value for *name* coerced to *type_*, or ``None`` for ``NULL``.

        Args:
            name: Column name (case-sensitive).
            type_: Target Python type: ``str``, ``int``, ``float``, ``bool``, or ``datetime``.

        Returns:
            The coerced value, or ``None`` when the column is ``NULL`` or absent.

        Raises:
            ValueError: When *type_* is not supported or conversion fails.
        """
        raw = self._values.get(name)
        if raw is None:
            return None
        if type_ is str:
            return raw
        if type_ is int:
            return int(raw)
        if type_ is float:
            return float(raw)
        if type_ is bool:
            return raw not in ("0", "false", "False", "")
        if type_ is datetime:
            return datetime.fromisoformat(raw)
        raise ValueError(f"Unsupported type {type_!r} for column {name!r}")

    def __getitem__(self, name: str) -> str | None:
        return self._values.get(name)

    def __repr__(self) -> str:
        return f"ECMDbRow({self._values!r})"


@dataclass
class ECMDbResult:
    """The parsed result of an ADO SQL query.

    Attributes:
        columns: Ordered list of column definitions from the schema section.
        rows: List of result rows, one per ``<z:row>`` element.
    """

    columns: list[ECMDbColumn]
    rows: list[ECMDbRow]

    def __iter__(self):
        return iter(self.rows)

    def __len__(self) -> int:
        return len(self.rows)

    def __repr__(self) -> str:
        return f"ECMDbResult(columns={len(self.columns)}, rows={len(self.rows)})"


class ECMAdoParser:
    """Parses the ADO Recordset XML format returned by ``ado.ExecuteSQL``.

    The XML uses the Microsoft ADO XML Persistence (XMLDATA) format with four
    namespaces (``s:``, ``dt:``, ``rs:``, ``z:``).  The ``<s:Schema>`` section
    describes the column layout; ``<rs:data>`` contains the actual ``<z:row>``
    elements.

    Args:
        xml: Raw bytes of the ADO XML response file.

    Example::

        result = client.execute(Jobs.ADO_EXECUTESQL, Flags=0, CursorType=-1,
                                Command="SELECT * FROM benutzer")
        parsed = ECMAdoParser(result.files[0].bytes()).parse()
        for row in parsed:
            print(row["benutzer"], row["osemail"])
    """

    def __init__(self, xml: bytes) -> None:
        self._root = _strip_namespaces(ET.fromstring(xml))

    def parse(self) -> ECMDbResult:
        """Parse the XML and return an :class:`ECMDbResult`.

        Returns:
            A fully populated :class:`ECMDbResult` with columns and rows.
        """
        columns = self._parse_columns()
        column_map = {c.name: c for c in columns}
        rows = self._parse_rows(column_map)
        return ECMDbResult(columns=columns, rows=rows)

    def _parse_columns(self) -> list[ECMDbColumn]:
        schema = self._root.find("Schema")
        if schema is None:
            return []
        element_type = schema.find("ElementType")
        if element_type is None:
            return []

        columns: list[ECMDbColumn] = []
        for attr_type in element_type.findall("AttributeType"):
            name = attr_type.get("name", "")
            number = int(attr_type.get("number", "0"))
            nullable = attr_type.get("nullable", "true") == "true"
            base_table = attr_type.get("basetable")
            base_column = attr_type.get("basecolumn")

            datatype_elem = attr_type.find("datatype")
            data_type = ""
            max_length = None
            if datatype_elem is not None:
                data_type = datatype_elem.get("type", "")
                raw_length = datatype_elem.get("maxLength")
                if raw_length is not None:
                    max_length = int(raw_length)

            columns.append(ECMDbColumn(
                name=name,
                number=number,
                data_type=data_type,
                max_length=max_length,
                nullable=nullable,
                base_table=base_table,
                base_column=base_column,
            ))

        columns.sort(key=lambda c: c.number)
        return columns

    def _parse_rows(self, column_map: dict[str, ECMDbColumn]) -> list[ECMDbRow]:
        data = self._root.find("data")
        if data is None:
            return []

        columns = list(column_map.values())
        rows: list[ECMDbRow] = []
        for z_row in data.findall("row"):
            values: dict[str, str | None] = {col.name: None for col in columns}
            for col_name in list(values):
                val = z_row.get(col_name)
                if val is not None:
                    values[col_name] = val
            rows.append(ECMDbRow(_columns=columns, _values=values))
        return rows
