"""
Output converters for SQL Server ODBC types unsupported by pyodbc.

Register converters via ``register_output_converters(conn)`` right after
``pyodbc.connect()`` and before any fetch calls.
"""

from __future__ import annotations

import struct as _struct

from collections.abc import Callable
from datetime import datetime, time, timedelta, timezone
from typing import TYPE_CHECKING


if TYPE_CHECKING:
    import pyodbc


# ODBC SQL type codes the Microsoft driver uses for types pyodbc can't marshal.
_UNSUPPORTED_ODBC_TYPE_CODES: dict[int, str] = {
    -16: "SQL_VARIANT / XML (driver-specific)",
    -150: "SQL_SS_VARIANT",
    -151: "SQL_SS_UDT (geography, geometry, hierarchyid, …)",
    -152: "SQL_SS_XML",
    -154: "SQL_SS_TIME2",
    -155: "SQL_SS_TIMESTAMPOFFSET",
}


def _decode_to_string(value: bytes) -> str | None:
    """Decode raw ODBC bytes to a Python string (UTF-16-LE, then UTF-8)."""
    if value is None:
        return None
    if isinstance(value, str):
        return value
    try:
        return value.decode("utf-16-le")
    except (UnicodeDecodeError, AttributeError):
        return value.decode("utf-8", errors="replace")


_NS_PER_US = 1000  # nanoseconds per microsecond
_MAX_MICROSECOND = 999_999  # upper bound for datetime.microsecond
_TIME2_PADDED_SIZE = 12  # byte threshold for padded TIME2 struct

# --- Struct layouts ----------------------------------------------------------

# SQL_SS_TIMESTAMPOFFSET_STRUCT (20 bytes, little-endian)
_DATETIMEOFFSET_STRUCT = _struct.Struct("<hHHHHHIhh")

# SQL_SS_TIME2_STRUCT — packed (10 bytes) or padded (12 bytes)
_TIME2_STRUCT_PACKED = _struct.Struct("<HHHI")
_TIME2_STRUCT_PADDED = _struct.Struct("<HHH2xI")


# --- Type-specific converters ------------------------------------------------


def _decode_datetimeoffset(value: bytes) -> datetime | None:
    """Unpack a 20-byte DATETIMEOFFSET struct into a tz-aware datetime."""
    if value is None:
        return None
    if isinstance(value, str):
        return value

    year, month, day, hour, minute, second, frac_ns, tz_h, tz_m = _DATETIMEOFFSET_STRUCT.unpack_from(value)
    microsecond = min(frac_ns // _NS_PER_US, _MAX_MICROSECOND)
    tz = timezone(timedelta(hours=tz_h, minutes=tz_m))
    return datetime(year, month, day, hour, minute, second, microsecond, tzinfo=tz)


def _decode_time2(value: bytes) -> time | None:
    """Unpack a TIME2 struct (10 or 12 bytes) into a time object."""
    if value is None:
        return None
    if isinstance(value, str):
        return value

    if len(value) >= _TIME2_PADDED_SIZE:
        hour, minute, second, frac_ns = _TIME2_STRUCT_PADDED.unpack_from(value)
    else:
        hour, minute, second, frac_ns = _TIME2_STRUCT_PACKED.unpack_from(value)
    microsecond = min(frac_ns // _NS_PER_US, _MAX_MICROSECOND)
    return time(hour, minute, second, microsecond)


# --- Registry ----------------------------------------------------------------

_SPECIFIC_CONVERTERS: dict[int, Callable[[bytes], object]] = {
    -155: _decode_datetimeoffset,
    -154: _decode_time2,
}


def register_output_converters(conn: pyodbc.Connection) -> None:
    """Register output converters for all unsupported SQL Server ODBC types."""
    for type_code in _UNSUPPORTED_ODBC_TYPE_CODES:
        converter = _SPECIFIC_CONVERTERS.get(type_code, _decode_to_string)
        conn.add_output_converter(type_code, converter)
