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

:class:`~xbrl_core.parser.RawContext` の XML 文字列を再パースし、
期間 (period)・Entity・Dimension を型付きオブジェクトに変換する。
"""

from __future__ import annotations

import datetime
import logging
from collections.abc import Iterator, Sequence
from dataclasses import dataclass

from lxml import etree

from xbrl_core.errors import XbrlParseError
from xbrl_core._namespaces import NS_XBRLI, NS_XBRLDI
from xbrl_core.parser import RawContext
from xbrl_core.periods import InstantPeriod, DurationPeriod, Period

__all__ = [
    "DimensionMember",
    "StructuredContext",
    "ContextCollection",
    "structure_contexts",
]

logger = logging.getLogger(__name__)

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


@dataclass(frozen=True, slots=True)
class DimensionMember:
    """Dimension の軸とメンバーの組。

    Attributes:
        axis: 軸の Clark notation（``"{namespace}localName"``）。
        member: メンバーの Clark notation（``"{namespace}localName"``）。
    """

    axis: str
    member: str


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

    Attributes:
        context_id: Context の ID。
        period: 期間情報。
        entity_id: Entity の identifier テキスト値。
        dimensions: Dimension メンバーのタプル。
        source_line: 元 XML の行番号。
        entity_scheme: Entity の identifier scheme 属性値。
    """

    context_id: str
    period: Period
    entity_id: str
    dimensions: tuple[DimensionMember, ...]
    source_line: int | None
    entity_scheme: str | None = None

    @property
    def is_instant(self) -> bool:
        """期間が InstantPeriod かどうかを判定する。"""
        return isinstance(self.period, InstantPeriod)

    @property
    def is_duration(self) -> bool:
        """期間が DurationPeriod かどうかを判定する。"""
        return isinstance(self.period, DurationPeriod)

    @property
    def has_dimensions(self) -> bool:
        """Dimension を持つかどうかを判定する。"""
        return len(self.dimensions) > 0

    @property
    def dimension_dict(self) -> dict[str, str]:
        """Dimension を辞書形式で返す。

        Returns:
            軸をキー、メンバーを値とする辞書。
        """
        return {dim.axis: dim.member for dim in self.dimensions}

    def has_dimension(self, axis: str) -> bool:
        """指定した軸の Dimension が存在するかを判定する。

        Args:
            axis: Clark notation の軸名。
        """
        return any(dim.axis == axis for dim in self.dimensions)

    def get_dimension_member(self, axis: str) -> str | None:
        """指定した軸に対応するメンバーを返す。

        Args:
            axis: Clark notation の軸名。

        Returns:
            メンバーの Clark notation。軸が存在しなければ None。
        """
        for dim in self.dimensions:
            if dim.axis == axis:
                return dim.member
        return None


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


def structure_contexts(
    raw_contexts: Sequence[RawContext],
) -> dict[str, StructuredContext]:
    """RawContext のシーケンスを構造化して辞書で返す。

    Args:
        raw_contexts: ``parse_xbrl_facts()`` が抽出した RawContext のシーケンス。

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

    Raises:
        XbrlParseError: period 欠落、日付不正、entity 欠落など
            構造解析に失敗した場合。
    """
    result: dict[str, StructuredContext] = {}
    for raw in raw_contexts:
        if raw.context_id is None:
            logger.debug(
                "Skipping RawContext with context_id=None (source_line=%s)",
                raw.source_line,
            )
            continue
        structured = _parse_single_context(raw)
        if raw.context_id in result:
            logger.debug(
                "Duplicate context_id=%r detected, overwriting",
                raw.context_id,
            )
        result[raw.context_id] = structured
    return result


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


def _parse_single_context(raw: RawContext) -> StructuredContext:
    """1 つの RawContext を StructuredContext に変換する。"""
    cid = raw.context_id or "(unknown)"
    try:
        elem = etree.fromstring(raw.xml.encode("utf-8"))  # noqa: S320
    except etree.XMLSyntaxError as exc:
        raise XbrlParseError(
            "XBRL_CTX_001",
            f"Failed to parse context XML (context_id={cid!r})",
            context_id=cid,
        ) from exc

    period = _parse_period(elem, cid)
    entity_id, entity_scheme = _parse_entity_id(elem, cid)
    dimensions = _parse_dimensions(elem, cid)

    return StructuredContext(
        context_id=cid,
        period=period,
        entity_id=entity_id,
        dimensions=dimensions,
        source_line=raw.source_line,
        entity_scheme=entity_scheme,
    )


def _parse_period(
    elem: etree._Element,
    cid: str,
) -> Period:
    """``<xbrli:period>`` を解析して Period を返す。"""
    period_elem = elem.find(f"{{{NS_XBRLI}}}period")
    if period_elem is None:
        raise XbrlParseError(
            "XBRL_CTX_002",
            f"Period element not found (context_id={cid!r})",
            context_id=cid,
        )

    instant_elem = period_elem.find(f"{{{NS_XBRLI}}}instant")
    if instant_elem is not None:
        instant = _parse_date(instant_elem.text, cid, "instant")
        return InstantPeriod(instant=instant)

    start_elem = period_elem.find(f"{{{NS_XBRLI}}}startDate")
    end_elem = period_elem.find(f"{{{NS_XBRLI}}}endDate")
    if start_elem is not None and end_elem is not None:
        start_date = _parse_date(start_elem.text, cid, "startDate")
        end_date = _parse_date(end_elem.text, cid, "endDate")
        return DurationPeriod(start_date=start_date, end_date=end_date)

    raise XbrlParseError(
        "XBRL_CTX_003",
        f"Invalid period children (context_id={cid!r})",
        context_id=cid,
    )


def _parse_date(
    text: str | None,
    cid: str,
    field_name: str,
) -> datetime.date:
    """ISO 8601 日付文字列を ``datetime.date`` に変換する。"""
    if text is None:
        raise XbrlParseError(
            "XBRL_CTX_004",
            f"Date text is empty for {field_name} (context_id={cid!r})",
            context_id=cid, field=field_name,
        )
    stripped = text.strip()
    if stripped == "":
        raise XbrlParseError(
            "XBRL_CTX_004",
            f"Date text is empty for {field_name} (context_id={cid!r})",
            context_id=cid, field=field_name,
        )
    try:
        return datetime.date.fromisoformat(stripped)
    except ValueError as exc:
        raise XbrlParseError(
            "XBRL_CTX_005",
            f"Invalid date format for {field_name}: {stripped!r} (context_id={cid!r})",
            context_id=cid, field=field_name, value=stripped,
        ) from exc


def _parse_entity_id(
    elem: etree._Element,
    cid: str,
) -> tuple[str, str | None]:
    """``<entity/identifier>`` からテキスト値と scheme 属性を抽出する。"""
    entity_elem = elem.find(f"{{{NS_XBRLI}}}entity")
    if entity_elem is None:
        raise XbrlParseError(
            "XBRL_CTX_006",
            f"Entity element not found (context_id={cid!r})",
            context_id=cid,
        )
    identifier_elem = entity_elem.find(f"{{{NS_XBRLI}}}identifier")
    if identifier_elem is None:
        raise XbrlParseError(
            "XBRL_CTX_007",
            f"Entity/identifier element not found (context_id={cid!r})",
            context_id=cid,
        )
    text = identifier_elem.text
    if text is None or text.strip() == "":
        raise XbrlParseError(
            "XBRL_CTX_008",
            f"Entity/identifier text is empty (context_id={cid!r})",
            context_id=cid,
        )
    scheme = identifier_elem.get("scheme")
    return text.strip(), scheme


def _parse_dimensions(
    elem: etree._Element,
    cid: str,
) -> tuple[DimensionMember, ...]:
    """``<scenario/explicitMember>`` から Dimension を抽出する。"""
    scenario_elem = elem.find(f"{{{NS_XBRLI}}}scenario")
    if scenario_elem is None:
        return ()

    members: list[DimensionMember] = []
    for explicit in scenario_elem.findall(f"{{{NS_XBRLDI}}}explicitMember"):
        dimension_attr = explicit.get("dimension")
        if dimension_attr is None:
            continue
        axis = _resolve_qname(dimension_attr.strip(), explicit.nsmap, cid)
        member_text = explicit.text
        if member_text is None or member_text.strip() == "":
            continue
        member = _resolve_qname(member_text.strip(), explicit.nsmap, cid)
        members.append(DimensionMember(axis=axis, member=member))

    return tuple(members)


def _resolve_qname(
    qname_str: str,
    nsmap: dict[str | None, str],
    cid: str,
) -> str:
    """``prefix:localName`` を Clark notation ``{ns}localName`` に変換する。"""
    if ":" not in qname_str:
        raise XbrlParseError(
            "XBRL_CTX_009",
            f"QName missing prefix (qname={qname_str!r}, context_id={cid!r})",
            context_id=cid, qname=qname_str,
        )
    prefix, local_name = qname_str.split(":", 1)
    uri = nsmap.get(prefix)
    if uri is None:
        raise XbrlParseError(
            "XBRL_CTX_010",
            f"QName prefix undefined (prefix={prefix!r}, context_id={cid!r})",
            context_id=cid, prefix=prefix, qname=qname_str,
        )
    return f"{{{uri}}}{local_name}"


# ---------------------------------------------------------------------------
# ContextCollection
# ---------------------------------------------------------------------------


class ContextCollection:
    """複数の StructuredContext に対するフィルタ・クエリ操作を提供する。

    汎用フィルタのみ提供する。連結/個別フィルタは上位パッケージ側に残す。
    """

    __slots__ = ("_contexts",)

    def __init__(self, contexts: dict[str, StructuredContext]) -> None:
        """コレクションを初期化する。

        Args:
            contexts: ``structure_contexts()`` の戻り値。
        """
        self._contexts: dict[str, StructuredContext] = dict(contexts)

    # --- Basic access ---

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

    def __iter__(self) -> Iterator[StructuredContext]:
        return iter(self._contexts.values())

    def __contains__(self, context_id: object) -> bool:
        return context_id in self._contexts

    def __getitem__(self, context_id: str) -> StructuredContext:
        return self._contexts[context_id]

    def get(
        self,
        context_id: str,
        default: StructuredContext | None = None,
    ) -> StructuredContext | None:
        """指定した context_id の StructuredContext を返す。

        Args:
            context_id: 取得対象の context_id。
            default: 見つからない場合のデフォルト値。

        Returns:
            StructuredContext。見つからなければ default。
        """
        return self._contexts.get(context_id, default)

    @property
    def as_dict(self) -> dict[str, StructuredContext]:
        """内部辞書の浅いコピーを返す。"""
        return dict(self._contexts)

    def __repr__(self) -> str:
        instant_count = sum(
            1 for ctx in self._contexts.values() if ctx.is_instant
        )
        duration_count = len(self._contexts) - instant_count
        return (
            f"ContextCollection("
            f"total={len(self._contexts)}, "
            f"instant={instant_count}, "
            f"duration={duration_count})"
        )

    # --- Filter methods ---

    def filter_instant(self) -> ContextCollection:
        """InstantPeriod のコンテキストのみを抽出する。"""
        return ContextCollection({
            cid: ctx
            for cid, ctx in self._contexts.items()
            if ctx.is_instant
        })

    def filter_duration(self) -> ContextCollection:
        """DurationPeriod のコンテキストのみを抽出する。"""
        return ContextCollection({
            cid: ctx
            for cid, ctx in self._contexts.items()
            if ctx.is_duration
        })

    def filter_by_period(self, period: Period) -> ContextCollection:
        """指定した期間に一致するコンテキストのみを抽出する。

        Args:
            period: フィルタ対象の期間。
        """
        return ContextCollection({
            cid: ctx
            for cid, ctx in self._contexts.items()
            if ctx.period == period
        })

    def filter_no_dimensions(self) -> ContextCollection:
        """Dimension が一切ないコンテキストを返す。"""
        return ContextCollection({
            cid: ctx
            for cid, ctx in self._contexts.items()
            if not ctx.has_dimensions
        })

    def filter_by_dimension(
        self,
        axis: str,
        member: str | None = None,
    ) -> ContextCollection:
        """指定した軸（とメンバー）を持つコンテキストのみを抽出する。

        Args:
            axis: Clark notation の軸名。
            member: Clark notation のメンバー名。None の場合は軸の存在のみ判定。
        """

        def _match(ctx: StructuredContext) -> bool:
            if member is None:
                return ctx.has_dimension(axis)
            return ctx.get_dimension_member(axis) == member

        return ContextCollection({
            cid: ctx
            for cid, ctx in self._contexts.items()
            if _match(ctx)
        })

    # --- Period queries ---

    @property
    def unique_instant_periods(self) -> tuple[InstantPeriod, ...]:
        """一意な InstantPeriod を instant 降順で返す。"""
        periods: set[InstantPeriod] = set()
        for ctx in self._contexts.values():
            if isinstance(ctx.period, InstantPeriod):
                periods.add(ctx.period)
        return tuple(sorted(
            periods,
            key=lambda p: p.instant,
            reverse=True,
        ))

    @property
    def unique_duration_periods(self) -> tuple[DurationPeriod, ...]:
        """一意な DurationPeriod を end_date 降順・start_date 昇順で返す。"""
        periods: set[DurationPeriod] = set()
        for ctx in self._contexts.values():
            if isinstance(ctx.period, DurationPeriod):
                periods.add(ctx.period)
        return tuple(sorted(
            periods,
            key=lambda p: (p.end_date, -p.start_date.toordinal()),
            reverse=True,
        ))

    @property
    def latest_instant_period(self) -> InstantPeriod | None:
        """最新の InstantPeriod を返す。存在しなければ None。"""
        periods = self.unique_instant_periods
        return periods[0] if periods else None

    @property
    def latest_duration_period(self) -> DurationPeriod | None:
        """最新の DurationPeriod を返す。存在しなければ None。"""
        periods = self.unique_duration_periods
        return periods[0] if periods else None

    def latest_instant_contexts(self) -> ContextCollection:
        """最新の InstantPeriod に一致するコンテキストを返す。"""
        period = self.latest_instant_period
        if period is None:
            return ContextCollection({})
        return self.filter_by_period(period)

    def latest_duration_contexts(self) -> ContextCollection:
        """最新の DurationPeriod に一致するコンテキストを返す。"""
        period = self.latest_duration_period
        if period is None:
            return ContextCollection({})
        return self.filter_by_period(period)
