"""RawUnit の XML 断片を構造化データに変換するモジュール。

:class:`~xbrl_core.parser.RawUnit` の XML 文字列を再パースし、
単位 (measure / divide) を型付きオブジェクトに変換する。
"""

from __future__ import annotations

import logging
import warnings
from collections.abc import Sequence
from dataclasses import dataclass

from lxml import etree

from xbrl_core.errors import XbrlParseError, XbrlWarning
from xbrl_core._namespaces import NS_ISO4217, NS_UTR, NS_XBRLI
from xbrl_core.parser import RawUnit

__all__ = [
    "SimpleMeasure",
    "DivideMeasure",
    "Measure",
    "StructuredUnit",
    "structure_units",
]

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

_WELL_KNOWN_PREFIXES: dict[str, str] = {
    "iso4217": NS_ISO4217,
    "xbrli": NS_XBRLI,
    "utr": NS_UTR,
}

# ---------------------------------------------------------------------------
# Data models
# ---------------------------------------------------------------------------


@dataclass(frozen=True, slots=True)
class SimpleMeasure:
    """単一の measure 要素を表す。

    Attributes:
        namespace_uri: 名前空間 URI（例: ``"http://www.xbrl.org/2003/iso4217"``）。
        local_name: ローカル名（例: ``"JPY"``）。
        raw_text: measure 要素のテキスト値（例: ``"iso4217:JPY"``）。
    """

    namespace_uri: str
    local_name: str
    raw_text: str


@dataclass(frozen=True, slots=True)
class DivideMeasure:
    """divide 要素（分子/分母）を表す。

    Attributes:
        numerator: 分子の SimpleMeasure。
        denominator: 分母の SimpleMeasure。
    """

    numerator: SimpleMeasure
    denominator: SimpleMeasure


Measure = SimpleMeasure | DivideMeasure
"""単位の measure 型。SimpleMeasure または DivideMeasure。"""


@dataclass(frozen=True, slots=True)
class StructuredUnit:
    """構造化された Unit。

    Attributes:
        unit_id: Unit の ID。
        measure: 単位情報。
        source_line: 元 XML の行番号。
    """

    unit_id: str
    measure: Measure
    source_line: int | None

    @property
    def is_monetary(self) -> bool:
        """通貨単位かどうかを返す。"""
        return (
            isinstance(self.measure, SimpleMeasure)
            and self.measure.namespace_uri == NS_ISO4217
        )

    @property
    def is_pure(self) -> bool:
        """純粋数値（xbrli:pure）かどうかを返す。"""
        return (
            isinstance(self.measure, SimpleMeasure)
            and self.measure.namespace_uri == NS_XBRLI
            and self.measure.local_name == "pure"
        )

    @property
    def is_shares(self) -> bool:
        """株式数（xbrli:shares）かどうかを返す。"""
        return (
            isinstance(self.measure, SimpleMeasure)
            and self.measure.namespace_uri == NS_XBRLI
            and self.measure.local_name == "shares"
        )

    @property
    def is_per_share(self) -> bool:
        """1株あたり単位（通貨/株式数）かどうかを返す。"""
        if not isinstance(self.measure, DivideMeasure):
            return False
        return (
            self.measure.numerator.namespace_uri == NS_ISO4217
            and self.measure.denominator.namespace_uri == NS_XBRLI
            and self.measure.denominator.local_name == "shares"
        )

    @property
    def currency_code(self) -> str | None:
        """通貨コードを返す。

        SimpleMeasure で iso4217 名前空間の場合はそのローカル名（例: ``"JPY"``）、
        DivideMeasure で分子が iso4217 名前空間の場合は分子のローカル名を返す。
        いずれにも該当しない場合は None。
        """
        if isinstance(self.measure, SimpleMeasure) and self.measure.namespace_uri == NS_ISO4217:
            return self.measure.local_name
        if isinstance(self.measure, DivideMeasure) and self.measure.numerator.namespace_uri == NS_ISO4217:
            return self.measure.numerator.local_name
        return None


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def structure_units(
    raw_units: Sequence[RawUnit],
    *,
    warning_class: type[Warning] = XbrlWarning,
    error_class: type[XbrlParseError] = XbrlParseError,
) -> dict[str, StructuredUnit]:
    """RawUnit のシーケンスを構造化して辞書で返す。

    Args:
        raw_units: ``parse_xbrl_facts()`` が抽出した RawUnit のシーケンス。
        warning_class: 警告に使用するクラス。
        error_class: エラーに使用するクラス。

    Returns:
        unit_id をキー、StructuredUnit を値とする辞書。
        重複 unit_id は後勝ち。

    Raises:
        XbrlParseError: measure 欠落、XML 構文エラーなど
            構造解析に失敗した場合。
    """
    result: dict[str, StructuredUnit] = {}
    for raw in raw_units:
        if raw.unit_id is None:
            logger.debug(
                "Skipping RawUnit with unit_id=None (source_line=%s)",
                raw.source_line,
            )
            continue
        structured = _parse_single_unit(raw, warning_class, error_class)
        if raw.unit_id in result:
            logger.debug(
                "Duplicate unit_id=%r detected, overwriting",
                raw.unit_id,
            )
        result[raw.unit_id] = structured
    return result


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


def _parse_single_unit(
    raw: RawUnit,
    warning_class: type[Warning] = XbrlWarning,
    error_class: type[XbrlParseError] = XbrlParseError,
) -> StructuredUnit:
    """1 つの RawUnit を StructuredUnit に変換する。"""
    uid = raw.unit_id or "(unknown)"
    try:
        elem = etree.fromstring(raw.xml.encode("utf-8"))  # noqa: S320
    except etree.XMLSyntaxError as exc:
        raise error_class(
            "XBRL_UNIT_001",
            f"Failed to parse unit XML (unit_id={uid!r})",
            unit_id=uid,
        ) from exc

    nsmap_str = {k: v for k, v in elem.nsmap.items() if k is not None}
    resolved_nsmap = {**_WELL_KNOWN_PREFIXES, **nsmap_str}

    divide_elem = elem.find(f"{{{NS_XBRLI}}}divide")
    if divide_elem is not None:
        measure = _parse_divide(divide_elem, resolved_nsmap, uid, warning_class, error_class)
    else:
        measure = _parse_simple_measure(elem, resolved_nsmap, uid, warning_class, error_class)

    return StructuredUnit(
        unit_id=uid,
        measure=measure,
        source_line=raw.source_line,
    )


def _parse_simple_measure(
    parent: etree._Element,
    nsmap: dict[str, str],
    uid: str,
    warning_class: type[Warning] = XbrlWarning,
    error_class: type[XbrlParseError] = XbrlParseError,
) -> SimpleMeasure:
    """parent 配下の measure 要素をパースして SimpleMeasure を返す。"""
    measures = parent.findall(f"{{{NS_XBRLI}}}measure")
    if not measures:
        raise error_class(
            "XBRL_UNIT_002",
            f"No measure element found (unit_id={uid!r})",
            unit_id=uid,
        )

    if len(measures) > 1:
        warnings.warn(
            f"Multiple measure elements detected, using first one "
            f"(unit_id={uid!r}, count={len(measures)})",
            warning_class,
            stacklevel=4,
        )

    return _resolve_measure_text(measures[0], nsmap, uid, warning_class, error_class)


def _parse_divide(
    divide_elem: etree._Element,
    nsmap: dict[str, str],
    uid: str,
    warning_class: type[Warning] = XbrlWarning,
    error_class: type[XbrlParseError] = XbrlParseError,
) -> DivideMeasure:
    """divide 要素をパースして DivideMeasure を返す。"""
    numerator_elem = divide_elem.find(f"{{{NS_XBRLI}}}unitNumerator")
    if numerator_elem is None:
        raise error_class(
            "XBRL_UNIT_003",
            f"divide missing unitNumerator (unit_id={uid!r})",
            unit_id=uid,
        )

    denominator_elem = divide_elem.find(f"{{{NS_XBRLI}}}unitDenominator")
    if denominator_elem is None:
        raise error_class(
            "XBRL_UNIT_004",
            f"divide missing unitDenominator (unit_id={uid!r})",
            unit_id=uid,
        )

    numerator = _parse_simple_measure(numerator_elem, nsmap, uid, warning_class, error_class)
    denominator = _parse_simple_measure(denominator_elem, nsmap, uid, warning_class, error_class)

    return DivideMeasure(numerator=numerator, denominator=denominator)


def _resolve_measure_text(
    measure_elem: etree._Element,
    nsmap: dict[str, str],
    uid: str,
    warning_class: type[Warning] = XbrlWarning,
    error_class: type[XbrlParseError] = XbrlParseError,
) -> SimpleMeasure:
    """measure 要素のテキストを解析して SimpleMeasure を返す。"""
    text = measure_elem.text
    if text is None or text.strip() == "":
        raise error_class(
            "XBRL_UNIT_005",
            f"Measure element has empty text (unit_id={uid!r})",
            unit_id=uid,
        )

    raw_text = text.strip()

    if ":" not in raw_text:
        warnings.warn(
            f"Measure has no prefix (unit_id={uid!r})",
            warning_class,
            stacklevel=4,
        )
        return SimpleMeasure(
            namespace_uri="",
            local_name=raw_text,
            raw_text=raw_text,
        )

    prefix, local_name = raw_text.split(":", 1)
    uri = nsmap.get(prefix)
    if uri is None:
        warnings.warn(
            f"Measure prefix is undefined, keeping raw_text "
            f"(prefix={prefix!r}, unit_id={uid!r})",
            warning_class,
            stacklevel=4,
        )
        return SimpleMeasure(
            namespace_uri="",
            local_name=local_name,
            raw_text=raw_text,
        )

    return SimpleMeasure(
        namespace_uri=uri,
        local_name=local_name,
        raw_text=raw_text,
    )
