from __future__ import annotations

import hashlib
import re
import unicodedata
from dataclasses import dataclass, field
from datetime import date, datetime
from typing import Any
from xml.etree import ElementTree as ET


def local_name(tag: str) -> str:
    return str(tag or "").split("}")[-1].split(":")[-1]


def normalise_key(value: str) -> str:
    return re.sub(r"[^a-z0-9]+", "", local_name(value).lower())


def text_value(node: ET.Element | None) -> str:
    if node is None:
        return ""
    return " ".join(part.strip() for part in node.itertext() if part.strip()).strip()


def first_value(values: dict[str, str], aliases: tuple[str, ...]) -> str:
    for alias in aliases:
        value = values.get(normalise_key(alias), "").strip()
        if value:
            return value
    return ""


def safe_int(value: Any, default: int = 0) -> int:
    match = re.search(r"-?\d+", str(value or ""))
    try:
        return int(match.group(0)) if match else default
    except (TypeError, ValueError):
        return default


_RU_MONTHS = {
    "январь": 1, "января": 1,
    "февраль": 2, "февраля": 2,
    "март": 3, "марта": 3,
    "апрель": 4, "апреля": 4,
    "май": 5, "мая": 5,
    "июнь": 6, "июня": 6,
    "июль": 7, "июля": 7,
    "август": 8, "августа": 8,
    "сентябрь": 9, "сентября": 9,
    "октябрь": 10, "октября": 10,
    "ноябрь": 11, "ноября": 11,
    "декабрь": 12, "декабря": 12,
}

_EN_MONTHS = {
    "january": 1, "jan": 1,
    "february": 2, "feb": 2,
    "march": 3, "mar": 3,
    "april": 4, "apr": 4,
    "may": 5,
    "june": 6, "jun": 6,
    "july": 7, "jul": 7,
    "august": 8, "aug": 8,
    "september": 9, "sep": 9, "sept": 9,
    "october": 10, "oct": 10,
    "november": 11, "nov": 11,
    "december": 12, "dec": 12,
}


def parse_date_value(value: str) -> date | None:
    # KHL/Stat2TV sometimes uses non-breaking/thin spaces and other Unicode
    # punctuation in localized dates. Normalize that input first so values like
    # "16 августа 2026, Вс" are parsed consistently on every OS/locale.
    value = unicodedata.normalize("NFKC", str(value or ""))
    value = value.replace("\u00a0", " ").replace("\u202f", " ").strip()
    value = re.sub(r"\s+", " ", value)
    if not value or value == "0000-00-00":
        return None

    iso_match = re.search(r"(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})", value)
    if iso_match:
        try:
            return date(
                int(iso_match.group(1)),
                int(iso_match.group(2)),
                int(iso_match.group(3)),
            )
        except ValueError:
            pass

    numeric_match = re.search(r"(\d{1,2})[./-](\d{1,2})[./-](\d{4})", value)
    if numeric_match:
        try:
            return date(
                int(numeric_match.group(3)),
                int(numeric_match.group(2)),
                int(numeric_match.group(1)),
            )
        except ValueError:
            pass

    # Stat2TV/KHL detail cards may return a localized human-readable value,
    # e.g. "16 августа 2026, Вс 13:00:00". Parse it without relying on
    # the PostgreSQL/OS locale so game_date always remains a real DATE.
    word_month_match = re.search(
        r"\b(\d{1,2})\s+([A-Za-zА-Яа-яЁё]+)\s+(\d{4})\b",
        value,
    )
    if word_month_match:
        month_name = word_month_match.group(2).casefold().strip(".,")
        month = _RU_MONTHS.get(month_name) or _EN_MONTHS.get(month_name)
        if month:
            try:
                return date(
                    int(word_month_match.group(3)),
                    month,
                    int(word_month_match.group(1)),
                )
            except ValueError:
                pass

    english_month_match = re.search(
        r"\b([A-Za-z]+)\s+(\d{1,2}),?\s+(\d{4})\b",
        value,
    )
    if english_month_match:
        month = _EN_MONTHS.get(english_month_match.group(1).casefold())
        if month:
            try:
                return date(
                    int(english_month_match.group(3)),
                    month,
                    int(english_month_match.group(2)),
                )
            except ValueError:
                pass

    for fmt in (
        "%Y%m%d",
        "%d%m%Y",
        "%Y-%m-%dT%H:%M:%S",
        "%Y-%m-%d %H:%M:%S",
    ):
        try:
            return datetime.strptime(value[:19], fmt).date()
        except ValueError:
            continue
    return None


def parse_time_value(value: str) -> str:
    value = str(value or "").strip()
    match = re.search(r"(?:T|\s|^)([0-2]?\d):([0-5]\d)(?::[0-5]\d)?", value)
    if not match:
        match = re.search(r"\b([0-2]?\d)[.-]([0-5]\d)\b", value)
    if not match:
        return value if re.fullmatch(r"\d{1,2}:\d{2}", value) else ""
    return f"{int(match.group(1)):02d}:{match.group(2)}"


@dataclass(slots=True)
class ParsedGame:
    external_id: str
    tournament_external_id: str
    tournament_type: str = ""

    game_number: str = ""
    round_number: str = ""
    round_name_ru: str = ""
    round_name_en: str = ""
    day_of_week: str = ""
    changes: int = 0

    game_date: date | None = None
    start_time: str = ""
    start_datetime_raw: str = ""
    status: str = "scheduled"
    approved: bool = False

    arena_external_id: str = ""
    arena_ru: str = ""
    arena_en: str = ""
    arena_city_ru: str = ""
    arena_city_en: str = ""

    home_team_external_id: str = ""
    away_team_external_id: str = ""
    home_entry_external_id: str = ""
    away_entry_external_id: str = ""
    home_team_name_ru: str = ""
    home_team_name_en: str = ""
    home_team_short_ru: str = ""
    home_team_short_en: str = ""
    away_team_name_ru: str = ""
    away_team_name_en: str = ""
    away_team_short_ru: str = ""
    away_team_short_en: str = ""
    home_team_city_ru: str = ""
    home_team_city_en: str = ""
    away_team_city_ru: str = ""
    away_team_city_en: str = ""

    home_score: int = 0
    away_score: int = 0
    score_raw: str = ""
    overtime_type: str = ""
    period_1_score: str = ""
    period_2_score: str = ""
    period_3_score: str = ""
    period_4_score: str = ""
    series_score: str = ""
    attendance: int = 0
    temporary_value: str = ""
    period_scores: str = ""
    raw_xml: str = ""

@dataclass(slots=True)
class MatchParseResult:
    games: list[ParsedGame] = field(default_factory=list)
    root_tag: str = ""
    item_tag: str = ""
    fields: list[str] = field(default_factory=list)
    candidate_nodes: int = 0
    confidence: float = 0.0
    sample: dict[str, Any] = field(default_factory=dict)


ID_ALIASES = (
    "id", "gameId", "game_id", "matchId", "match_id", "eventId",
    "event_id", "uid", "gameUid", "game_id_khl",
)
TOURNAMENT_ALIASES = (
    "tournamentId", "tournament_id", "competitionId", "championshipId",
    "seasonId", "stageId",
)
DATE_ALIASES = (
    "date", "gameDate", "game_date", "startDate", "start_date",
    "eventDate", "scheduledDate", "datetime", "dateTime",
    "startDateTime", "start_datetime", "dt",
)
TIME_ALIASES = (
    "time", "gameTime", "startTime", "start_time", "eventTime",
    "scheduledTime", "beginTime", "datetime", "dateTime", "startDateTime",
)
STATUS_ALIASES = (
    "status", "gameStatus", "state", "phase", "resultType", "finished",
)
NUMBER_ALIASES = (
    "number", "gameNumber", "game_number", "matchNumber", "num", "n",
)
ARENA_RU_ALIASES = (
    "arena", "arenaName", "arenaRu", "stadium", "venue", "place",
    "rink", "location",
)
ARENA_EN_ALIASES = (
    "arenaEn", "arenaNameEn", "stadiumEn", "venueEn", "placeEn",
)

HOME_ID_ALIASES = (
    "homeTeamId", "home_id", "team1Id", "teamAId", "hostId",
    "firstTeamId", "teamHomeId",
)
AWAY_ID_ALIASES = (
    "awayTeamId", "away_id", "team2Id", "teamBId", "guestId",
    "secondTeamId", "teamAwayId",
)
HOME_RU_ALIASES = (
    "homeTeam", "homeTeamName", "homeName", "team1", "team1Name",
    "teamA", "host", "hostName", "firstTeam", "teamHome",
)
HOME_EN_ALIASES = (
    "homeTeamEn", "homeTeamNameEn", "homeNameEn", "team1En",
    "team1NameEn", "teamAEn", "hostEn", "firstTeamEn",
)
AWAY_RU_ALIASES = (
    "awayTeam", "awayTeamName", "awayName", "team2", "team2Name",
    "teamB", "guest", "guestName", "secondTeam", "teamAway",
)
AWAY_EN_ALIASES = (
    "awayTeamEn", "awayTeamNameEn", "awayNameEn", "team2En",
    "team2NameEn", "teamBEn", "guestEn", "secondTeamEn",
)
HOME_CITY_RU_ALIASES = ("homeCity", "team1City", "hostCity", "homeTeamCity")
HOME_CITY_EN_ALIASES = ("homeCityEn", "team1CityEn", "hostCityEn")
AWAY_CITY_RU_ALIASES = ("awayCity", "team2City", "guestCity", "awayTeamCity")
AWAY_CITY_EN_ALIASES = ("awayCityEn", "team2CityEn", "guestCityEn")
HOME_SCORE_ALIASES = (
    "homeScore", "scoreHome", "score1", "team1Score", "goals1",
    "hostScore", "firstScore",
)
AWAY_SCORE_ALIASES = (
    "awayScore", "scoreAway", "score2", "team2Score", "goals2",
    "guestScore", "secondScore",
)
PERIOD_SCORE_ALIASES = (
    "periodScores", "periodScore", "scoreByPeriods", "periods", "sets",
)


def flatten_node(node: ET.Element) -> dict[str, str]:
    values: dict[str, str] = {}

    for key, value in node.attrib.items():
        if value is not None:
            values.setdefault(normalise_key(key), str(value).strip())

    for child in list(node):
        child_key = normalise_key(child.tag)
        value = text_value(child)
        if value:
            values.setdefault(child_key, value)
        for key, attr_value in child.attrib.items():
            attr_key = normalise_key(f"{local_name(child.tag)}_{key}")
            values.setdefault(attr_key, str(attr_value).strip())
            values.setdefault(normalise_key(key), str(attr_value).strip())

        for grandchild in list(child):
            grand_key = normalise_key(
                f"{local_name(child.tag)}_{local_name(grandchild.tag)}"
            )
            grand_value = text_value(grandchild)
            if grand_value:
                values.setdefault(grand_key, grand_value)
                values.setdefault(normalise_key(grandchild.tag), grand_value)
            for key, attr_value in grandchild.attrib.items():
                values.setdefault(
                    normalise_key(
                        f"{local_name(child.tag)}_{local_name(grandchild.tag)}_{key}"
                    ),
                    str(attr_value).strip(),
                )
    return values


def nested_team(node: ET.Element, side: str) -> dict[str, str]:
    side_names = {
        "home": {
            "home", "hometeam", "team1", "teama", "host", "firstteam",
        },
        "away": {
            "away", "awayteam", "team2", "teamb", "guest", "secondteam",
        },
    }[side]
    for child in node.iter():
        if child is node:
            continue
        if normalise_key(child.tag) not in side_names:
            continue
        attrs = {normalise_key(k): str(v).strip() for k, v in child.attrib.items()}
        flat = flatten_node(child)
        merged = {**flat, **attrs}
        return {
            "id": first_value(merged, ("id", "teamId", "clubId", "externalId")),
            "name_ru": first_value(
                merged, ("name", "nameRu", "title", "teamName", "shortName")
            ) or text_value(child),
            "name_en": first_value(
                merged, ("nameEn", "titleEn", "teamNameEn", "shortNameEn")
            ),
            "city_ru": first_value(merged, ("city", "cityRu", "town")),
            "city_en": first_value(merged, ("cityEn", "townEn")),
            "score": first_value(merged, ("score", "goals", "result")),
        }
    return {}


def looks_like_game_node(node: ET.Element, values: dict[str, str]) -> bool:
    tag = normalise_key(node.tag)
    if tag in {"game", "match", "event", "fixture", "calendaritem"}:
        return True
    has_date = bool(first_value(values, DATE_ALIASES))
    has_home = bool(
        first_value(values, HOME_RU_ALIASES + HOME_ID_ALIASES)
        or nested_team(node, "home")
    )
    has_away = bool(
        first_value(values, AWAY_RU_ALIASES + AWAY_ID_ALIASES)
        or nested_team(node, "away")
    )
    return has_date and has_home and has_away



def parse_score_pair(value: str) -> tuple[int, int]:
    match = re.fullmatch(r"\s*(\d+)\s*:\s*(\d+)\s*", str(value or ""))
    if not match:
        return 0, 0
    return int(match.group(1)), int(match.group(2))


def truthy(value: Any) -> bool:
    return str(value or "").strip().lower() in {
        "1", "true", "yes", "on", "approved",
    }


def derive_schedule_status(attrs: dict[str, str]) -> str:
    approved = truthy(attrs.get("approved"))
    score = str(attrs.get("score") or "").strip()
    temporary = str(attrs.get("temp") or "").strip()

    if approved:
        return "finished"
    if temporary or re.fullmatch(r"\s*\d+\s*:\s*\d+\s*", score):
        return "live"
    return "scheduled"


def parsed_schedule_game(
    node: ET.Element,
    *,
    tournament_external_id: str,
    tournament_type: str,
) -> ParsedGame | None:
    attrs = {str(key): str(value or "") for key, value in node.attrib.items()}
    external_id = attrs.get("id", "").strip()
    if not external_id:
        return None

    score_raw = attrs.get("score", "").strip()
    home_score, away_score = parse_score_pair(score_raw)
    period_values = [
        attrs.get("scP1", "").strip(),
        attrs.get("scP2", "").strip(),
        attrs.get("scP3", "").strip(),
        attrs.get("scP4", "").strip(),
    ]
    return ParsedGame(
        external_id=external_id,
        tournament_external_id=(
            attrs.get("tnId", "").strip() or tournament_external_id
        ),
        tournament_type=tournament_type,
        game_number=attrs.get("number", "").strip(),
        round_number=attrs.get("round", "").strip(),
        round_name_ru=attrs.get("roundname", "").strip(),
        round_name_en=attrs.get("roundname_en", "").strip(),
        day_of_week=attrs.get("dayofweek", "").strip(),
        changes=safe_int(attrs.get("changes")),
        game_date=parse_date_value(attrs.get("date", "")),
        start_time=parse_time_value(attrs.get("time", "")),
        start_datetime_raw=(
            f"{attrs.get('date', '').strip()} "
            f"{attrs.get('time', '').strip()}"
        ).strip(),
        status=derive_schedule_status(attrs),
        approved=truthy(attrs.get("approved")),
        arena_external_id=attrs.get("arenaid", "").strip(),
        arena_ru=attrs.get("arena", "").strip(),
        arena_en=attrs.get("arena_en", "").strip(),
        arena_city_ru=attrs.get("arena_city", "").strip(),
        arena_city_en=attrs.get("arena_city_en", "").strip(),
        home_team_external_id=attrs.get("teama", "").strip(),
        away_team_external_id=attrs.get("teamb", "").strip(),
        home_entry_external_id=attrs.get("homeId", "").strip(),
        away_entry_external_id=attrs.get("visitorId", "").strip(),
        home_team_name_ru=attrs.get("homeName", "").strip(),
        home_team_name_en=attrs.get("homeName_en", "").strip(),
        home_team_short_ru=attrs.get("homeName_l3", "").strip(),
        home_team_short_en=attrs.get("homeName_l3_en", "").strip(),
        away_team_name_ru=attrs.get("visitorName", "").strip(),
        away_team_name_en=attrs.get("visitorName_en", "").strip(),
        away_team_short_ru=attrs.get("visitorName_l3", "").strip(),
        away_team_short_en=attrs.get("visitorName_l3_en", "").strip(),
        home_team_city_ru=attrs.get("homeCity", "").strip(),
        home_team_city_en=attrs.get("homeCity_en", "").strip(),
        away_team_city_ru=attrs.get("visitorCity", "").strip(),
        away_team_city_en=attrs.get("visitorCity_en", "").strip(),
        home_score=home_score,
        away_score=away_score,
        score_raw=score_raw,
        overtime_type=attrs.get("ots", "").strip(),
        period_1_score=period_values[0],
        period_2_score=period_values[1],
        period_3_score=period_values[2],
        period_4_score=period_values[3],
        series_score=attrs.get("sscore", "").strip(),
        attendance=safe_int(attrs.get("attendance")),
        temporary_value=attrs.get("temp", "").strip(),
        period_scores="|".join(period_values),
        raw_xml=ET.tostring(node, encoding="unicode"),
    )


def parse_stat2tv_schedule(
    root: ET.Element,
    *,
    tournament_external_id: str,
) -> MatchParseResult:
    result = MatchParseResult(root_tag=local_name(root.tag), item_tag="Game")
    tournament_id = str(
        root.attrib.get("tournamentId") or tournament_external_id
    ).strip()
    tournament_type = str(
        root.attrib.get("tournamentType") or ""
    ).strip().lower()
    field_names: set[str] = set()

    for node in root.findall("./Game"):
        field_names.update(str(key) for key in node.attrib)
        game = parsed_schedule_game(
            node,
            tournament_external_id=tournament_id,
            tournament_type=tournament_type,
        )
        if game is not None:
            result.games.append(game)

    result.candidate_nodes = len(result.games)
    result.fields = sorted(field_names)
    result.confidence = 1.0 if result.games else 0.0
    if result.games:
        sample = result.games[0]
        result.sample = {
            "external_id": sample.external_id,
            "date": sample.game_date.isoformat() if sample.game_date else None,
            "time": sample.start_time,
            "home": sample.home_team_name_ru or sample.home_team_name_en,
            "away": sample.away_team_name_ru or sample.away_team_name_en,
            "status": sample.status,
            "approved": sample.approved,
            "score": sample.score_raw,
            "series_score": sample.series_score,
            "tournament_type": sample.tournament_type,
        }
    return result


def parse_stat2tv_game_by_id(
    content: bytes | str,
    *,
    tournament_external_id: str,
    game_external_id: str,
) -> ParsedGame | None:
    """Parse only one game from a Stat2TV schedule response."""
    raw = content.encode("utf-8") if isinstance(content, str) else content
    root = ET.fromstring(raw)
    if normalise_key(root.tag) != "schedule":
        result = parse_games_xml(
            raw,
            tournament_external_id=tournament_external_id,
        )
        return next(
            (
                game for game in result.games
                if str(game.external_id) == str(game_external_id)
            ),
            None,
        )

    tournament_id = str(
        root.attrib.get("tournamentId") or tournament_external_id
    ).strip()
    tournament_type = str(
        root.attrib.get("tournamentType") or ""
    ).strip().lower()
    node = root.find(f"./Game[@id='{str(game_external_id)}']")
    if node is None:
        return None
    return parsed_schedule_game(
        node,
        tournament_external_id=tournament_id,
        tournament_type=tournament_type,
    )


def parse_games_xml(
    content: bytes | str,
    *,
    tournament_external_id: str,
) -> MatchParseResult:
    raw = content.encode("utf-8") if isinstance(content, str) else content
    root = ET.fromstring(raw)
    if (
        normalise_key(root.tag) == "schedule"
        and root.findall("./Game")
    ):
        return parse_stat2tv_schedule(
            root,
            tournament_external_id=tournament_external_id,
        )

    result = MatchParseResult(root_tag=local_name(root.tag))

    nodes: list[tuple[ET.Element, dict[str, str]]] = []
    for node in root.iter():
        if node is root:
            continue
        values = flatten_node(node)
        if looks_like_game_node(node, values):
            nodes.append((node, values))

    # Avoid parsing nested team/event nodes as games when their parent is already a game.
    node_ids = {id(node) for node, _ in nodes}
    filtered_nodes: list[tuple[ET.Element, dict[str, str]]] = []
    for node, values in nodes:
        parent_is_candidate = False
        for parent, _ in nodes:
            if parent is node:
                continue
            if any(child is node for child in parent.iter()):
                parent_is_candidate = True
                break
        if not parent_is_candidate:
            filtered_nodes.append((node, values))

    if not filtered_nodes:
        filtered_nodes = nodes

    result.candidate_nodes = len(filtered_nodes)
    seen: set[str] = set()
    field_names: set[str] = set()

    for index, (node, values) in enumerate(filtered_nodes, start=1):
        field_names.update(values.keys())
        home_nested = nested_team(node, "home")
        away_nested = nested_team(node, "away")

        raw_datetime = first_value(values, DATE_ALIASES)
        raw_time = first_value(values, TIME_ALIASES)
        game_date = parse_date_value(raw_datetime)
        start_time = parse_time_value(raw_time or raw_datetime)

        external_id = first_value(values, ID_ALIASES)
        if not external_id:
            material = (
                f"{tournament_external_id}|{raw_datetime}|"
                f"{first_value(values, HOME_RU_ALIASES)}|"
                f"{first_value(values, AWAY_RU_ALIASES)}|{index}"
            )
            external_id = "auto-" + hashlib.sha1(
                material.encode("utf-8", errors="ignore")
            ).hexdigest()[:20]

        if external_id in seen:
            continue
        seen.add(external_id)

        game = ParsedGame(
            external_id=external_id,
            tournament_external_id=(
                first_value(values, TOURNAMENT_ALIASES)
                or tournament_external_id
            ),
            game_number=first_value(values, NUMBER_ALIASES),
            game_date=game_date,
            start_time=start_time,
            start_datetime_raw=raw_datetime or raw_time,
            status=first_value(values, STATUS_ALIASES),
            arena_ru=first_value(values, ARENA_RU_ALIASES),
            arena_en=first_value(values, ARENA_EN_ALIASES),
            home_team_external_id=(
                first_value(values, HOME_ID_ALIASES)
                or home_nested.get("id", "")
            ),
            away_team_external_id=(
                first_value(values, AWAY_ID_ALIASES)
                or away_nested.get("id", "")
            ),
            home_team_name_ru=(
                first_value(values, HOME_RU_ALIASES)
                or home_nested.get("name_ru", "")
            ),
            home_team_name_en=(
                first_value(values, HOME_EN_ALIASES)
                or home_nested.get("name_en", "")
            ),
            away_team_name_ru=(
                first_value(values, AWAY_RU_ALIASES)
                or away_nested.get("name_ru", "")
            ),
            away_team_name_en=(
                first_value(values, AWAY_EN_ALIASES)
                or away_nested.get("name_en", "")
            ),
            home_team_city_ru=(
                first_value(values, HOME_CITY_RU_ALIASES)
                or home_nested.get("city_ru", "")
            ),
            home_team_city_en=(
                first_value(values, HOME_CITY_EN_ALIASES)
                or home_nested.get("city_en", "")
            ),
            away_team_city_ru=(
                first_value(values, AWAY_CITY_RU_ALIASES)
                or away_nested.get("city_ru", "")
            ),
            away_team_city_en=(
                first_value(values, AWAY_CITY_EN_ALIASES)
                or away_nested.get("city_en", "")
            ),
            home_score=safe_int(
                first_value(values, HOME_SCORE_ALIASES)
                or home_nested.get("score", "")
            ),
            away_score=safe_int(
                first_value(values, AWAY_SCORE_ALIASES)
                or away_nested.get("score", "")
            ),
            period_scores=first_value(values, PERIOD_SCORE_ALIASES),
            raw_xml=ET.tostring(node, encoding="unicode"),
        )
        result.games.append(game)

    if result.games:
        result.item_tag = local_name(filtered_nodes[0][0].tag)
        complete = sum(
            1 for game in result.games
            if game.game_date and game.home_team_name_ru and game.away_team_name_ru
        )
        result.confidence = round(complete / max(1, len(result.games)), 3)
        sample_game = result.games[0]
        result.sample = {
            "external_id": sample_game.external_id,
            "date": sample_game.game_date.isoformat() if sample_game.game_date else None,
            "time": sample_game.start_time,
            "home": sample_game.home_team_name_ru or sample_game.home_team_name_en,
            "away": sample_game.away_team_name_ru or sample_game.away_team_name_en,
            "status": sample_game.status,
        }

    result.fields = sorted(field_names)
    return result
