"""Combine commander selections across partner/background modes."""
from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import Iterable, Sequence, Tuple

from exceptions import CommanderPartnerError

from code.deck_builder.partner_background_utils import analyze_partner_background
from code.deck_builder.color_identity_utils import canon_color_code, color_label_from_code

_WUBRG_ORDER: Tuple[str, ...] = ("W", "U", "B", "R", "G", "C")
_COLOR_PRIORITY = {color: index for index, color in enumerate(_WUBRG_ORDER)}


class PartnerMode(str, Enum):
    """Enumerates supported partner mechanics."""

    NONE = "none"
    PARTNER = "partner"
    PARTNER_WITH = "partner_with"
    BACKGROUND = "background"
    DOCTOR_COMPANION = "doctor_companion"


@dataclass(frozen=True, slots=True)
class CombinedCommander:
    """Represents merged commander metadata for deck building."""

    primary_name: str
    secondary_name: str | None
    partner_mode: PartnerMode
    color_identity: Tuple[str, ...]
    theme_tags: Tuple[str, ...]
    raw_tags_primary: Tuple[str, ...]
    raw_tags_secondary: Tuple[str, ...]
    warnings: Tuple[str, ...]
    color_code: str = ""
    color_label: str = ""
    primary_color_identity: Tuple[str, ...] = ()
    secondary_color_identity: Tuple[str, ...] = ()


@dataclass(frozen=True)
class _CommanderData:
    name: str
    display_name: str
    color_identity: Tuple[str, ...]
    themes: Tuple[str, ...]
    raw_tags: Tuple[str, ...]
    partner_with: Tuple[str, ...]
    is_partner: bool
    supports_backgrounds: bool
    is_background: bool
    is_doctor: bool
    is_doctors_companion: bool

    @classmethod
    def from_source(cls, source: object) -> "_CommanderData":
        name = _get_attr(source, "name") or _get_attr(source, "display_name") or ""
        display_name = _get_attr(source, "display_name") or name
        if not display_name:
            raise CommanderPartnerError("Commander is missing a display name", details={"source": repr(source)})

        color_identity = _normalize_colors(_get_attr(source, "color_identity") or _get_attr(source, "colorIdentity"))
        themes = _normalize_theme_tags(
            _get_attr(source, "themes")
            or _get_attr(source, "theme_tags")
            or _get_attr(source, "themeTags")
        )
        raw_tags = tuple(_ensure_sequence(_get_attr(source, "themes") or _get_attr(source, "theme_tags") or _get_attr(source, "themeTags")))

        partner_with: Tuple[str, ...] = tuple(_ensure_sequence(_get_attr(source, "partner_with") or ()))
        oracle_text = _get_attr(source, "oracle_text") or _get_attr(source, "text")
        type_line = _get_attr(source, "type_line") or _get_attr(source, "type")

        detection = analyze_partner_background(type_line, oracle_text, raw_tags or themes)
        if not partner_with:
            partner_with = detection.partner_with

        is_partner = bool(_get_attr(source, "is_partner")) or detection.has_partner
        supports_backgrounds = bool(_get_attr(source, "supports_backgrounds")) or detection.choose_background
        is_background = bool(_get_attr(source, "is_background")) or detection.is_background
        is_doctor = bool(_get_attr(source, "is_doctor")) or detection.is_doctor
        is_doctors_companion = bool(_get_attr(source, "is_doctors_companion")) or detection.is_doctors_companion

        return cls(
            name=name,
            display_name=display_name,
            color_identity=color_identity,
            themes=themes,
            raw_tags=tuple(raw_tags),
            partner_with=partner_with,
            is_partner=is_partner,
            supports_backgrounds=supports_backgrounds,
            is_background=is_background,
            is_doctor=is_doctor,
            is_doctors_companion=is_doctors_companion,
        )


def build_combined_commander(
    primary: object,
    secondary: object | None,
    mode: PartnerMode,
) -> CombinedCommander:
    """Merge commander metadata according to the selected partner mode."""

    primary_data = _CommanderData.from_source(primary)
    secondary_data = _CommanderData.from_source(secondary) if secondary is not None else None

    _validate_mode_inputs(primary_data, secondary_data, mode)
    warnings = _collect_warnings(primary_data, secondary_data)

    color_identity = _merge_colors(primary_data, secondary_data)
    theme_tags = _merge_theme_tags(primary_data.themes, secondary_data.themes if secondary_data else ())
    raw_secondary = secondary_data.raw_tags if secondary_data else tuple()

    secondary_name = secondary_data.display_name if secondary_data else None
    color_code = canon_color_code(color_identity)
    color_label = color_label_from_code(color_code)
    primary_colors = primary_data.color_identity
    secondary_colors = secondary_data.color_identity if secondary_data else tuple()

    return CombinedCommander(
        primary_name=primary_data.display_name,
        secondary_name=secondary_name,
        partner_mode=mode,
        color_identity=color_identity,
        theme_tags=theme_tags,
        raw_tags_primary=primary_data.raw_tags,
        raw_tags_secondary=raw_secondary,
        warnings=warnings,
        color_code=color_code,
        color_label=color_label,
        primary_color_identity=primary_colors,
        secondary_color_identity=secondary_colors,
    )


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _validate_mode_inputs(
    primary: _CommanderData,
    secondary: _CommanderData | None,
    mode: PartnerMode,
) -> None:
    details = {
        "mode": mode.value,
        "primary": primary.display_name,
        "secondary": secondary.display_name if secondary else None,
    }

    if mode is PartnerMode.NONE:
        if secondary is not None:
            raise CommanderPartnerError("Secondary commander provided but partner mode is NONE", details=details)
        return

    if secondary is None:
        raise CommanderPartnerError("Secondary commander is required for selected partner mode", details=details)

    _ensure_distinct(primary, secondary, details)

    if mode is PartnerMode.PARTNER:
        if not primary.is_partner:
            raise CommanderPartnerError(f"{primary.display_name} does not have Partner", details=details)
        if not secondary.is_partner:
            raise CommanderPartnerError(f"{secondary.display_name} does not have Partner", details=details)
        if secondary.is_background:
            raise CommanderPartnerError("Selected secondary is a Background; choose partner mode BACKGROUND", details=details)
        return

    if mode is PartnerMode.PARTNER_WITH:
        _validate_partner_with(primary, secondary, details)
        return

    if mode is PartnerMode.BACKGROUND:
        _validate_background(primary, secondary, details)
        return

    if mode is PartnerMode.DOCTOR_COMPANION:
        _validate_doctor_companion(primary, secondary, details)
        return

    raise CommanderPartnerError("Unsupported partner mode", details=details)


def _ensure_distinct(primary: _CommanderData, secondary: _CommanderData, details: dict[str, object]) -> None:
    if primary.display_name.casefold() == secondary.display_name.casefold():
        raise CommanderPartnerError("Primary and secondary commanders must be different", details=details)


def _validate_partner_with(primary: _CommanderData, secondary: _CommanderData, details: dict[str, object]) -> None:
    if secondary.is_background:
        raise CommanderPartnerError("Background cannot be used in PARTNER_WITH mode", details=details)
    if not primary.partner_with:
        raise CommanderPartnerError(f"{primary.display_name} does not specify a Partner With target", details=details)
    if not secondary.partner_with:
        raise CommanderPartnerError(f"{secondary.display_name} does not specify a Partner With target", details=details)

    secondary_names = {_standardize_name(name) for name in secondary.partner_with}
    primary_names = {_standardize_name(name) for name in primary.partner_with}
    if _standardize_name(secondary.display_name) not in primary_names:
        raise CommanderPartnerError(
            f"{secondary.display_name} is not a legal Partner With target for {primary.display_name}",
            details=details,
        )
    if _standardize_name(primary.display_name) not in secondary_names:
        raise CommanderPartnerError(
            f"{primary.display_name} is not a legal Partner With target for {secondary.display_name}",
            details=details,
        )


def _validate_background(primary: _CommanderData, secondary: _CommanderData, details: dict[str, object]) -> None:
    if not secondary.is_background:
        raise CommanderPartnerError("Selected secondary commander is not a Background", details=details)
    if not primary.supports_backgrounds:
        raise CommanderPartnerError(f"{primary.display_name} cannot choose a Background", details=details)
    if primary.is_background:
        raise CommanderPartnerError("Background cannot be used as primary commander", details=details)

def _validate_doctor_companion(primary: _CommanderData, secondary: _CommanderData, details: dict[str, object]) -> None:
    primary_is_doctor = bool(primary.is_doctor)
    primary_is_companion = bool(primary.is_doctors_companion)
    secondary_is_doctor = bool(secondary.is_doctor)
    secondary_is_companion = bool(secondary.is_doctors_companion)

    if not (primary_is_doctor or primary_is_companion):
        raise CommanderPartnerError(f"{primary.display_name} is not a Doctor or Doctor's Companion", details=details)
    if not (secondary_is_doctor or secondary_is_companion):
        raise CommanderPartnerError(f"{secondary.display_name} is not a Doctor or Doctor's Companion", details=details)

    if primary_is_doctor and secondary_is_doctor:
        raise CommanderPartnerError("Doctor commanders must pair with a Doctor's Companion", details=details)
    if primary_is_companion and secondary_is_companion:
        raise CommanderPartnerError("Doctor's Companion must pair with a Doctor", details=details)

    # Ensure pairing is complementary doctor <-> companion
    if primary_is_doctor and not secondary_is_companion:
        raise CommanderPartnerError(f"{secondary.display_name} is not a legal Doctor's Companion", details=details)
    if primary_is_companion and not secondary_is_doctor:
        raise CommanderPartnerError(f"{secondary.display_name} is not a legal Doctor pairing", details=details)


def _collect_warnings(
    primary: _CommanderData,
    secondary: _CommanderData | None,
) -> Tuple[str, ...]:
    warnings: list[str] = []
    if primary.is_partner and primary.supports_backgrounds:
        warnings.append(
            f"{primary.display_name} has both Partner and Background abilities; ensure the selected mode is intentional."
        )
    if secondary and secondary.is_partner and secondary.supports_backgrounds:
        warnings.append(
            f"{secondary.display_name} has both Partner and Background abilities; ensure the selected mode is intentional."
        )
    return tuple(warnings)


def _merge_colors(primary: _CommanderData, secondary: _CommanderData | None) -> Tuple[str, ...]:
    colors = set(primary.color_identity)
    if secondary:
        colors.update(secondary.color_identity)
    if not colors:
        return tuple()
    return tuple(sorted(colors, key=lambda color: (_COLOR_PRIORITY.get(color, len(_COLOR_PRIORITY)), color)))


def _merge_theme_tags(*sources: Iterable[str]) -> Tuple[str, ...]:
    seen: set[str] = set()
    merged: list[str] = []
    for source in sources:
        for tag in source:
            clean = tag.strip()
            if not clean:
                continue
            key = clean.casefold()
            if key in seen:
                continue
            seen.add(key)
            merged.append(clean)
    return tuple(merged)


def _normalize_colors(colors: Sequence[str] | None) -> Tuple[str, ...]:
    if not colors:
        return tuple()
    normalized = [str(color).strip().upper() for color in colors]
    normalized = [color for color in normalized if color]
    return tuple(normalized)


def _normalize_theme_tags(tags: Sequence[str] | None) -> Tuple[str, ...]:
    if not tags:
        return tuple()
    return tuple(str(tag).strip() for tag in tags if str(tag).strip())


def _ensure_sequence(value: object) -> Sequence[str]:
    if value is None:
        return ()
    if isinstance(value, (list, tuple)):
        return value
    return (value,)


def _standardize_name(name: str) -> str:
    return name.strip().casefold()


def _get_attr(source: object, attr: str) -> object:
    return getattr(source, attr, None)


__all__ = [
    "CombinedCommander",
    "PartnerMode",
    "build_combined_commander",
]
