"""Definition Linkbase パーサー。

XBRL Definition Linkbase (`_def.xml`) をパースし、
ハイパーキューブ構造（Table → Axis → Domain → Member）を
木構造として提供する。
"""

from __future__ import annotations

import logging
import warnings
from dataclasses import dataclass, field

from lxml import etree

from xbrl_core.errors import XbrlParseError, XbrlWarning
from xbrl_core._linkbase_utils import (
    ConceptExtractor,
    extract_concept_from_href as _extract_concept_from_href,
)
from xbrl_core._namespaces import NS_LINK, NS_XBRLDT, NS_XLINK

logger = logging.getLogger(__name__)

# ---------- arcrole 定数 ----------
ARCROLE_ALL = "http://xbrl.org/int/dim/arcrole/all"
"""``all`` arcrole URI。"""

ARCROLE_HYPERCUBE_DIMENSION = "http://xbrl.org/int/dim/arcrole/hypercube-dimension"
"""``hypercube-dimension`` arcrole URI。"""

ARCROLE_DIMENSION_DOMAIN = "http://xbrl.org/int/dim/arcrole/dimension-domain"
"""``dimension-domain`` arcrole URI。"""

ARCROLE_DIMENSION_DEFAULT = "http://xbrl.org/int/dim/arcrole/dimension-default"
"""``dimension-default`` arcrole URI。"""

ARCROLE_DOMAIN_MEMBER = "http://xbrl.org/int/dim/arcrole/domain-member"
"""``domain-member`` arcrole URI。"""

# ========== データモデル ==========


@dataclass(frozen=True, slots=True)
class DefinitionArc:
    """Definition Linkbase の 1 本の arc。

    Attributes:
        from_concept: 親 concept のローカル名。
        to_concept: 子 concept のローカル名。
        from_href: 親 concept の xlink:href。
        to_href: 子 concept の xlink:href。
        arcrole: arcrole URI。
        order: 表示順。
        closed: xbrldt:closed 属性値。``all`` arcrole のアークでのみ設定される。
        context_element: xbrldt:contextElement 属性値。``all`` arcrole のアークでのみ設定される。
        usable: usable 属性値。
    """

    from_concept: str
    to_concept: str
    from_href: str
    to_href: str
    arcrole: str
    order: float
    closed: bool | None = None
    context_element: str | None = None
    usable: bool = True


@dataclass(frozen=True, slots=True)
class MemberNode:
    """ドメイン配下のメンバーノード（ツリー構造）。

    Attributes:
        concept: メンバーのローカル名。
        href: xlink:href。
        usable: Fact を持ちうるか。
        children: 子メンバーノードのタプル（order 順ソート済み）。
    """

    concept: str
    href: str
    usable: bool = True
    children: tuple[MemberNode, ...] = ()

    def __repr__(self) -> str:
        return (
            f"MemberNode(concept={self.concept!r}, "
            f"usable={self.usable}, children={len(self.children)})"
        )


@dataclass(frozen=True, slots=True)
class AxisInfo:
    """1 つの Axis（次元軸）の構造化情報。

    Attributes:
        axis_concept: Axis のローカル名。
        order: hypercube-dimension arc の order 値。
        domain: ドメインのルートノード。
        default_member: デフォルトメンバーのローカル名。
    """

    axis_concept: str
    order: float = 0.0
    domain: MemberNode | None = None
    default_member: str | None = None


@dataclass(frozen=True, slots=True)
class HypercubeInfo:
    """ハイパーキューブの構造化情報。

    Attributes:
        table_concept: Table のローカル名。
        heading_concept: Heading（ルート要素）のローカル名。
        axes: ハイパーキューブに属する Axis の情報。
        closed: ハイパーキューブが closed かどうか。
        context_element: ``"segment"`` or ``"scenario"``。
        line_items_concept: LineItems のローカル名。
    """

    table_concept: str
    heading_concept: str
    axes: tuple[AxisInfo, ...]
    closed: bool
    context_element: str
    line_items_concept: str | None = None

    def __repr__(self) -> str:
        axes = ", ".join(a.axis_concept for a in self.axes)
        return (
            f"HypercubeInfo(table={self.table_concept!r}, "
            f"axes=[{axes}])"
        )


@dataclass(frozen=True, slots=True)
class DefinitionTree:
    """1 つの role URI に対応する定義ツリー。

    Attributes:
        role_uri: ロール URI。
        arcs: 全 arc のタプル。
        hypercubes: 構造化されたハイパーキューブ情報のタプル。
    """

    role_uri: str
    arcs: tuple[DefinitionArc, ...]
    hypercubes: tuple[HypercubeInfo, ...]

    @property
    def has_hypercube(self) -> bool:
        """ハイパーキューブ構造を含むかどうか。"""
        return len(self.hypercubes) > 0

    def __repr__(self) -> str:
        return (
            f"DefinitionTree(role_uri={self.role_uri!r}, "
            f"arcs={len(self.arcs)}, hypercubes={len(self.hypercubes)})"
        )


@dataclass(frozen=True, slots=True)
class DefinitionLinkbase:
    """定義リンクベースのパース結果全体。

    Attributes:
        source_path: ソースファイルパス（エラーメッセージ用）。
        trees: role_uri → DefinitionTree の辞書。
    """

    source_path: str | None
    trees: dict[str, DefinitionTree]
    _children_index: dict[tuple[str, str], tuple[DefinitionArc, ...]] = field(
        init=False, repr=False, compare=False,
    )
    _parent_index: dict[tuple[str, str], tuple[DefinitionArc, ...]] = field(
        init=False, repr=False, compare=False,
    )

    def __repr__(self) -> str:
        return (
            f"DefinitionLinkbase(roles={len(self.trees)}, "
            f"arcs={sum(len(t.arcs) for t in self.trees.values())})"
        )

    def __post_init__(self) -> None:
        """内部インデックスを構築する。"""
        object.__setattr__(self, "_children_index", self._build_children_index())
        object.__setattr__(self, "_parent_index", self._build_parent_index())

    def _build_children_index(
        self,
    ) -> dict[tuple[str, str], tuple[DefinitionArc, ...]]:
        """子インデックスを構築する。"""
        index: dict[tuple[str, str], list[DefinitionArc]] = {}
        for tree in self.trees.values():
            for arc in tree.arcs:
                key = (tree.role_uri, arc.from_concept)
                index.setdefault(key, []).append(arc)
        return {
            k: tuple(sorted(v, key=lambda a: a.order)) for k, v in index.items()
        }

    def _build_parent_index(
        self,
    ) -> dict[tuple[str, str], tuple[DefinitionArc, ...]]:
        """親インデックスを構築する。"""
        index: dict[tuple[str, str], list[DefinitionArc]] = {}
        for tree in self.trees.values():
            for arc in tree.arcs:
                key = (tree.role_uri, arc.to_concept)
                index.setdefault(key, []).append(arc)
        return {k: tuple(v) for k, v in index.items()}

    @property
    def role_uris(self) -> tuple[str, ...]:
        """全ロール URI のタプルを返す。"""
        return tuple(self.trees.keys())

    def get_tree(self, role_uri: str) -> DefinitionTree | None:
        """指定ロールの定義ツリーを返す。

        Args:
            role_uri: ロール URI。

        Returns:
            DefinitionTree。存在しない場合は ``None``。
        """
        return self.trees.get(role_uri)

    def children_of(
        self,
        parent: str,
        *,
        role_uri: str | None = None,
        arcrole: str | None = None,
    ) -> tuple[DefinitionArc, ...]:
        """指定 concept の子アークを返す。

        Args:
            parent: 親 concept のローカル名。
            role_uri: ロール URI。``None`` の場合は全ロールを横断する。
            arcrole: arcrole URI でフィルタ。``None`` の場合はフィルタなし。

        Returns:
            子アークのタプル。
        """
        if role_uri is not None:
            arcs = self._children_index.get((role_uri, parent), ())
        else:
            result: list[DefinitionArc] = []
            for tree in self.trees.values():
                result.extend(
                    self._children_index.get((tree.role_uri, parent), ())
                )
            result.sort(key=lambda a: a.order)
            arcs = tuple(result)

        if arcrole is not None:
            arcs = tuple(a for a in arcs if a.arcrole == arcrole)
        return arcs

    def parent_of(
        self,
        child: str,
        *,
        role_uri: str | None = None,
        arcrole: str | None = None,
    ) -> tuple[DefinitionArc, ...]:
        """指定 concept の親アークを返す。

        Args:
            child: 子 concept のローカル名。
            role_uri: ロール URI。``None`` の場合は全ロールを横断する。
            arcrole: arcrole URI でフィルタ。``None`` の場合はフィルタなし。

        Returns:
            親アークのタプル。
        """
        if role_uri is not None:
            arcs = self._parent_index.get((role_uri, child), ())
        else:
            result: list[DefinitionArc] = []
            for tree in self.trees.values():
                result.extend(
                    self._parent_index.get((tree.role_uri, child), ())
                )
            result.sort(key=lambda a: a.order)
            arcs = tuple(result)

        if arcrole is not None:
            arcs = tuple(a for a in arcs if a.arcrole == arcrole)
        return arcs


# ========== プライベートヘルパー ==========


def _build_member_tree(
    concept: str,
    href: str,
    usable: bool,
    arcs: list[DefinitionArc],
    visited: set[str],
) -> MemberNode:
    """domain-member arc を再帰的に辿り MemberNode ツリーを構築する。"""
    visited = visited | {concept}  # 兄弟間独立のイミュータブルコピー

    child_arcs = sorted(
        [
            a
            for a in arcs
            if a.arcrole == ARCROLE_DOMAIN_MEMBER and a.from_concept == concept
        ],
        key=lambda a: a.order,
    )

    children: list[MemberNode] = []
    for arc in child_arcs:
        if arc.to_concept in visited:
            logger.warning(
                "定義リンク: domain-member 循環参照を検出: %s → %s",
                concept,
                arc.to_concept,
            )
            continue
        child_node = _build_member_tree(
            concept=arc.to_concept,
            href=arc.to_href,
            usable=arc.usable,
            arcs=arcs,
            visited=visited,
        )
        children.append(child_node)

    return MemberNode(
        concept=concept,
        href=href,
        usable=usable,
        children=tuple(children),
    )


def _build_hypercubes(arcs: list[DefinitionArc]) -> list[HypercubeInfo]:
    """DefinitionArc 群からハイパーキューブ構造を構築する。"""
    hypercubes: list[HypercubeInfo] = []

    for arc in arcs:
        if arc.arcrole != ARCROLE_ALL:
            continue

        heading = arc.from_concept
        table = arc.to_concept
        closed = arc.closed if arc.closed is not None else False
        context_element = arc.context_element if arc.context_element is not None else "scenario"

        # この table に属する axes を収集
        axis_arcs = [
            a
            for a in arcs
            if a.arcrole == ARCROLE_HYPERCUBE_DIMENSION
            and a.from_concept == table
        ]

        # 各 axis の domain, default, members を収集
        axis_infos: list[AxisInfo] = []
        for axis_arc in axis_arcs:
            axis_concept = axis_arc.to_concept

            domain_arcs = [
                a
                for a in arcs
                if a.arcrole == ARCROLE_DIMENSION_DOMAIN
                and a.from_concept == axis_concept
            ]
            domain_node: MemberNode | None = None
            if domain_arcs:
                da = domain_arcs[0]
                domain_node = _build_member_tree(
                    concept=da.to_concept,
                    href=da.to_href,
                    usable=da.usable,
                    arcs=arcs,
                    visited=set(),
                )

            default_member: str | None = None
            for a in arcs:
                if (
                    a.arcrole == ARCROLE_DIMENSION_DEFAULT
                    and a.from_concept == axis_concept
                ):
                    default_member = a.to_concept
                    break

            axis_infos.append(
                AxisInfo(
                    axis_concept=axis_concept,
                    order=axis_arc.order,
                    domain=domain_node,
                    default_member=default_member,
                )
            )

        axis_infos.sort(key=lambda a: a.order)

        # heading から domain-member で接続される *LineItems を特定
        line_items_arcs = [
            a
            for a in arcs
            if a.arcrole == ARCROLE_DOMAIN_MEMBER
            and a.from_concept == heading
            and a.to_concept.endswith("LineItems")
        ]
        line_items_concept: str | None = None
        if line_items_arcs:
            line_items_concept = min(
                line_items_arcs, key=lambda a: a.order
            ).to_concept

        hypercubes.append(
            HypercubeInfo(
                table_concept=table,
                heading_concept=heading,
                axes=tuple(axis_infos),
                closed=closed,
                context_element=context_element,
                line_items_concept=line_items_concept,
            )
        )

    return hypercubes


# ========== 公開 API ==========


def parse_definition_linkbase(
    xml_bytes: bytes,
    *,
    source_path: str | None = None,
    concept_extractor: ConceptExtractor = _extract_concept_from_href,
    warning_class: type[Warning] = XbrlWarning,
    error_class: type[XbrlParseError] = XbrlParseError,
) -> DefinitionLinkbase:
    """Definition Linkbase の XML bytes をパースする。

    Args:
        xml_bytes: ``_def.xml`` の bytes。
        source_path: エラーメッセージに含めるソースパス（任意）。
        concept_extractor: href から concept 名を抽出する関数。
        warning_class: 警告に使用するクラス。
        error_class: エラーに使用するクラス。

    Returns:
        パース結果の DefinitionLinkbase。

    Raises:
        XbrlParseError: XML パースに失敗した場合。
    """
    try:
        root = etree.fromstring(xml_bytes)  # noqa: S320
    except etree.XMLSyntaxError as e:
        path_info = f": {source_path}" if source_path else ""
        raise error_class(
            "XBRL_LINK_003",
            f"Failed to parse Definition Linkbase XML{path_info}",
            source=source_path,
        ) from e

    def_links = root.findall(f"{{{NS_LINK}}}definitionLink")
    logger.debug("定義リンクベースをパース中: %d definitionLink 要素", len(def_links))

    trees: dict[str, DefinitionTree] = {}

    for def_link in def_links:
        role_uri = def_link.get(f"{{{NS_XLINK}}}role")
        if not role_uri:
            warnings.warn(
                "定義リンク: xlink:role が未設定のためスキップします",
                warning_class,
                stacklevel=2,
            )
            continue

        # loc マップ構築
        loc_map: dict[str, tuple[str, str]] = {}
        for loc_elem in def_link.findall(f"{{{NS_LINK}}}loc"):
            label = loc_elem.get(f"{{{NS_XLINK}}}label")
            href = loc_elem.get(f"{{{NS_XLINK}}}href")
            if not label or not href:
                continue
            concept = concept_extractor(href)
            if concept is None:
                warnings.warn(
                    f"定義リンク: loc の href にフラグメントがありません: '{href}'",
                    warning_class,
                    stacklevel=2,
                )
                continue
            loc_map[label] = (concept, href)

        # definitionArc → DefinitionArc に変換
        arc_list: list[DefinitionArc] = []
        for arc_elem in def_link.findall(f"{{{NS_LINK}}}definitionArc"):
            from_label = arc_elem.get(f"{{{NS_XLINK}}}from")
            to_label = arc_elem.get(f"{{{NS_XLINK}}}to")
            arcrole = arc_elem.get(f"{{{NS_XLINK}}}arcrole", "")

            if not from_label or not to_label:
                continue

            if from_label not in loc_map:
                warnings.warn(
                    f"定義リンク: 不明なロケーター参照 '{from_label}' をスキップします",
                    warning_class,
                    stacklevel=2,
                )
                continue
            if to_label not in loc_map:
                warnings.warn(
                    f"定義リンク: 不明なロケーター参照 '{to_label}' をスキップします",
                    warning_class,
                    stacklevel=2,
                )
                continue

            from_concept, from_href = loc_map[from_label]
            to_concept, to_href = loc_map[to_label]

            # order の解析
            order_str = arc_elem.get("order")
            order: float = 0.0
            if order_str is not None:
                try:
                    order = float(order_str)
                except ValueError:
                    warnings.warn(
                        f"定義リンク: order 値 '{order_str}' が不正です。0.0 を使用します",
                        warning_class,
                        stacklevel=2,
                    )

            # usable の解析
            usable_str = arc_elem.get("usable")
            usable = usable_str not in ("false", "0") if usable_str is not None else True

            # closed / contextElement は all arcrole のみ
            closed: bool | None = None
            context_element: str | None = None
            if arcrole == ARCROLE_ALL:
                closed_str = arc_elem.get(f"{{{NS_XBRLDT}}}closed")
                if closed_str is not None:
                    closed = closed_str in ("true", "1")
                context_element = arc_elem.get(
                    f"{{{NS_XBRLDT}}}contextElement"
                )

            arc_list.append(
                DefinitionArc(
                    from_concept=from_concept,
                    to_concept=to_concept,
                    from_href=from_href,
                    to_href=to_href,
                    arcrole=arcrole,
                    order=order,
                    closed=closed,
                    context_element=context_element,
                    usable=usable,
                )
            )

        logger.debug(
            "ロール %s: %d loc, %d arc",
            role_uri,
            len(loc_map),
            len(arc_list),
        )

        # ハイパーキューブ構造化
        hypercubes = _build_hypercubes(arc_list)

        trees[role_uri] = DefinitionTree(
            role_uri=role_uri,
            arcs=tuple(arc_list),
            hypercubes=tuple(hypercubes),
        )

    total_hc = sum(len(t.hypercubes) for t in trees.values())
    logger.info(
        "定義リンクベースのパース完了: %d ロール, %d ハイパーキューブ",
        len(trees),
        total_hc,
    )

    return DefinitionLinkbase(source_path=source_path, trees=trees)
