from __future__ import annotations

from collections.abc import Iterable
from datetime import date

import pandas as pd
from tqdm.auto import tqdm

from .console import console
from .config import ProjectPaths
from .io import load_csv_optional, read_csv, write_csv, write_lines
from .mal import MalClient


ANIME_DETAIL_FIELDS = (
    "mean",
    "num_list_users",
    "genres",
    "synopsis",
    "main_picture",
    "media_type",
)


def _current_season_name(month: int) -> str:
    if month <= 3:
        return "winter"
    if month <= 6:
        return "spring"
    if month <= 9:
        return "summer"
    return "fall"


def iter_recent_seasons(years_back: int) -> Iterable[tuple[int, str]]:
    today = date.today()
    seasons = ("winter", "spring", "summer", "fall")
    current_year = today.year
    current_season = _current_season_name(today.month)

    for year in range(current_year - years_back, current_year + 1):
        for season in seasons:
            if year == current_year and seasons.index(season) > seasons.index(current_season):
                break
            yield year, season


def anime_detail_to_record(detail: dict) -> dict:
    genres = []
    for genre in detail.get("genres", []):
        name = genre.get("name", "").strip()
        if name:
            genres.append(name)

    main_picture = detail.get("main_picture", {}) or {}
    return {
        "anime_id": int(detail["id"]),
        "num_list_users": int(detail.get("num_list_users") or 0),
        "title": detail.get("title", ""),
        "mean": float(detail.get("mean") or 0.0),
        "genres": ", ".join(genres),
        "synopsis": detail.get("synopsis", "") or "",
        "image_url": main_picture.get("large") or main_picture.get("medium") or "",
        "media_type": detail.get("media_type", "") or "",
    }


def _read_ids_from_file(path) -> set[int]:
    ids: set[int] = set()
    if not path.exists():
        return ids
    with open(path, "r", encoding="utf-8") as infile:
        for line in infile:
            value = line.strip()
            if value.isdigit():
                ids.add(int(value))
    return ids


def collect_candidate_anime_ids(
    paths: ProjectPaths,
    *,
    include_recent_seasons: bool = True,
    years_back: int = 2,
    client: MalClient | None = None,
) -> set[int]:
    anime_ids: set[int] = set()

    interactions_path = paths.resolve_user_interactions_input()
    if interactions_path.exists():
        interactions_df = read_csv(interactions_path, usecols=["anime_id"])
        anime_ids.update(interactions_df["anime_id"].astype(int).tolist())

    catalog_path = paths.resolve_anime_catalog_input()
    if catalog_path.exists():
        catalog_df = read_csv(catalog_path, usecols=["anime_id"])
        anime_ids.update(catalog_df["anime_id"].astype(int).tolist())

    anime_ids.update(_read_ids_from_file(paths.anime_ids))

    if include_recent_seasons:
        season_client = client or MalClient.from_env()
        seasons = list(iter_recent_seasons(years_back))
        with tqdm(total=len(seasons), desc="Scanning seasons", unit="season", dynamic_ncols=True) as bar:
            for year, season in seasons:
                for item in season_client.iter_seasonal_anime(year, season):
                    node = item.get("node", {})
                    anime_id = node.get("id")
                    if anime_id is not None:
                        anime_ids.add(int(anime_id))
                bar.update(1)
                bar.set_postfix(season=f"{season} {year}", anime_ids=len(anime_ids))

    return anime_ids


def merge_relationships(
    catalog_df: pd.DataFrame,
    relationships_df: pd.DataFrame,
) -> pd.DataFrame:
    if relationships_df.empty:
        return catalog_df
    merged = catalog_df.drop(columns=["relationship"], errors="ignore").merge(
        relationships_df[["anime_id", "relationship"]],
        on="anime_id",
        how="left",
    )
    return merged


def refresh_anime_catalog(
    paths: ProjectPaths,
    *,
    years_back: int = 2,
    include_recent_seasons: bool = True,
    refresh_existing: bool = False,
    sleep_seconds: float = 0.0,
) -> dict:
    paths.ensure_directories()
    console.info(
        "Refreshing anime catalog from interactions, existing catalog, saved anime ids, and recent MAL seasons"
    )
    client = MalClient.from_env(sleep_seconds=sleep_seconds)

    existing_catalog_path = paths.resolve_anime_catalog_input()
    existing_catalog_df = load_csv_optional(existing_catalog_path)
    existing_records: dict[int, dict] = {}
    if not existing_catalog_df.empty:
        existing_catalog_df["anime_id"] = existing_catalog_df["anime_id"].astype(int)
        for row in existing_catalog_df.to_dict(orient="records"):
            existing_records[int(row["anime_id"])] = row
    console.info(f"Existing catalog entries available: {len(existing_records)}")

    candidate_ids = collect_candidate_anime_ids(
        paths,
        include_recent_seasons=include_recent_seasons,
        years_back=years_back,
        client=client,
    )
    console.info(f"Unique candidate anime ids collected: {len(candidate_ids)}")

    if refresh_existing:
        ids_to_fetch = sorted(candidate_ids | set(existing_records))
    else:
        ids_to_fetch = sorted(candidate_ids - set(existing_records))
    console.info(
        f"Anime metadata fetch plan: {len(ids_to_fetch)} to fetch, {len(candidate_ids) - len(ids_to_fetch)} reused"
    )

    with tqdm(total=len(ids_to_fetch), desc="Refreshing anime", unit="anime", dynamic_ncols=True) as bar:
        for anime_id in ids_to_fetch:
            detail = client.get_anime_details(anime_id, fields=ANIME_DETAIL_FIELDS, allow_missing=True)
            if detail is None:
                bar.update(1)
                bar.set_postfix(anime_id=anime_id, skipped=True)
                continue
            record = anime_detail_to_record(detail)
            existing_records[anime_id] = record
            bar.update(1)
            bar.set_postfix(anime_id=anime_id, title=record["title"][:24], users=record["num_list_users"])

            if bar.n % 50 == 0:
                interim_df = pd.DataFrame(
                    existing_records.values(),
                    columns=[
                        "anime_id",
                        "num_list_users",
                        "title",
                        "mean",
                        "genres",
                        "synopsis",
                        "image_url",
                        "media_type",
                        "relationship",
                    ],
                )
                if not interim_df.empty:
                    interim_df = interim_df.sort_values("anime_id")
                relationships_df = load_csv_optional(paths.resolve_anime_relationships_input())
                interim_df = merge_relationships(interim_df, relationships_df)
                write_csv(interim_df, paths.anime_catalog, paths.legacy_anime_catalog)

    catalog_df = pd.DataFrame(
        existing_records.values(),
        columns=[
            "anime_id",
            "num_list_users",
            "title",
            "mean",
            "genres",
            "synopsis",
            "image_url",
            "media_type",
            "relationship",
        ],
    )
    if not catalog_df.empty:
        catalog_df = catalog_df.sort_values("anime_id")
    relationships_df = load_csv_optional(paths.resolve_anime_relationships_input())
    catalog_df = merge_relationships(catalog_df, relationships_df)

    write_csv(catalog_df, paths.anime_catalog, paths.legacy_anime_catalog)
    write_lines((str(anime_id) for anime_id in sorted(candidate_ids)), paths.anime_ids)
    console.success(
        f"Catalog refresh wrote {len(catalog_df)} anime records to {paths.anime_catalog}"
    )

    return {
        "catalog_path": str(paths.anime_catalog),
        "anime_count": int(len(catalog_df)),
        "fetched_count": int(len(ids_to_fetch)),
        "reused_count": int(len(candidate_ids) - len(ids_to_fetch)),
    }
