import re
from dataclasses import dataclass
from datetime import date
from io import BytesIO
from typing import cast

from fastapi import Depends
from PIL import ImageFont
from PIL.Image import Image
from PIL.ImageFont import FreeTypeFont

from .advent_image import _XY, AdventImage
from .calendar_config import CalendarConfig, get_calendar_config
from .config import Config, get_config
from .helpers import (
    RE_TTF,
    EventDates,
    Random,
    list_fonts,
    list_images_auto,
    list_images_manual,
    load_image,
    set_len,
)
from .settings import WEBDAV

RE_NUM = re.compile(r"/(\d+)\.", flags=re.IGNORECASE)


async def get_all_sorted_days(
    cal_cfg: CalendarConfig = Depends(get_calendar_config),
) -> list[int]:
    """
    Alle Tage, für die es ein Türchen gibt
    """

    return sorted(set(door.day for door in cal_cfg.doors))


async def get_all_parts(
    cfg: Config = Depends(get_config),
    days: list[int] = Depends(get_all_sorted_days),
) -> dict[int, str]:
    """
    Lösung auf vorhandene Tage aufteilen
    """

    # noch keine Buchstaben verteilt
    result = {day: "" for day in days}
    # extra-Tage ausfiltern
    days = [day for day in days if day not in cfg.puzzle.extra_days]

    solution_length = len(cfg.solution.clean)
    num_days = len(days)

    rnd = await Random.get()
    solution_days = [
        # wie oft passen die Tage "ganz" in die Länge der Lösung?
        # zB 26 Buchstaben // 10 Tage == 2 mal => 2 Zeichen pro Tag
        *rnd.shuffled(days * (solution_length // num_days)),
        # wie viele Buchstaben bleiben übrig?
        # zB 26 % 10 == 6 Buchstaben => an 6 Tagen ein Zeichen mehr
        *rnd.sample(days, solution_length % num_days),
    ]

    for day, letter in zip(solution_days, cfg.solution.clean):
        result[day] += letter

    return result


async def get_all_event_dates(
    cfg: Config = Depends(get_config),
    days: list[int] = Depends(get_all_sorted_days),
    parts: dict[int, str] = Depends(get_all_parts),
) -> EventDates:
    """
    Aktueller Kalender-Zeitraum
    """

    if cfg.puzzle.skip_empty:
        days = [day for day in days if parts[day] != "" or day in cfg.puzzle.extra_days]

    return EventDates(
        today=date.today(),
        begin_month=cfg.puzzle.begin_month,
        begin_day=cfg.puzzle.begin_day,
        events=days,
        close_after=cfg.puzzle.close_after,
    )


async def get_all_auto_image_names(
    days: list[int] = Depends(get_all_sorted_days),
    images: list[str] = Depends(list_images_auto),
) -> dict[int, str]:
    """
    Bilder: Reihenfolge zufällig bestimmen
    """

    rnd = await Random.get()
    ls = set_len(images, len(days))

    return dict(zip(days, rnd.shuffled(ls)))


async def get_all_manual_image_names(
    manual_image_names: list[str] = Depends(list_images_manual),
) -> dict[int, str]:
    """
    Bilder: "manual" zuordnen
    """

    return {
        int(num_match.group(1)): name
        for name in manual_image_names
        if (num_match := RE_NUM.search(name)) is not None
    }


async def get_all_image_names(
    auto_image_names: dict[int, str] = Depends(get_all_auto_image_names),
    manual_image_names: dict[int, str] = Depends(get_all_manual_image_names),
) -> dict[int, str]:
    """
    Bilder "auto" und "manual" zu Tagen zuordnen
    """

    result = auto_image_names.copy()
    result.update(manual_image_names)

    return result


@dataclass(slots=True, frozen=True)
class TTFont:
    # Dateiname
    file_name: str

    # Schriftgröße für den Font
    size: int = 50

    @property
    async def font(self) -> FreeTypeFont:
        return ImageFont.truetype(
            font=BytesIO(await WEBDAV.read_bytes(self.file_name)),
            size=100,
        )


async def get_all_ttfonts(
    font_names: list[str] = Depends(list_fonts),
) -> list[TTFont]:
    result = []

    for name in font_names:
        assert (size_match := RE_TTF.search(name)) is not None

        result.append(
            TTFont(
                file_name=name,
                size=int(size_match.group(1)),
            )
        )

    return result


async def gen_day_auto_image(
    day: int,
    cfg: Config,
    auto_image_names: dict[int, str],
    day_parts: dict[int, str],
    ttfonts: list[TTFont],
) -> Image:
    """
    Automatisch generiertes Bild erstellen
    """

    # Datei existiert garantiert!
    img = await load_image(auto_image_names[day])
    image = await AdventImage.from_img(img, cfg)

    rnd = await Random.get(day)
    xy_range = range(cfg.image.border, (cfg.image.size - cfg.image.border))

    # Buchstaben verstecken
    for letter in day_parts[day]:
        await image.hide_text(
            xy=cast(_XY, tuple(rnd.choices(xy_range, k=2))),
            text=letter,
            font=await rnd.choice(ttfonts).font,
        )

    return image.img


async def get_day_image(
    day: int,
    days: list[int] = Depends(get_all_sorted_days),
    cfg: Config = Depends(get_config),
    manual_image_names: dict[int, str] = Depends(get_all_manual_image_names),
    auto_image_names: dict[int, str] = Depends(get_all_auto_image_names),
    day_parts: dict[int, str] = Depends(get_all_parts),
    ttfonts: list[TTFont] = Depends(get_all_ttfonts),
) -> Image | None:
    """
    Bild für einen Tag abrufen
    """

    if day not in days:
        return None

    try:
        # Versuche "manual"-Bild zu laden
        img = await load_image(manual_image_names[day])

        # Als AdventImage verarbeiten
        image = await AdventImage.from_img(img, cfg)
        return image.img

    except KeyError, RuntimeError:
        # Erstelle automatisch generiertes Bild
        return await gen_day_auto_image(
            day=day,
            cfg=cfg,
            auto_image_names=auto_image_names,
            day_parts=day_parts,
            ttfonts=ttfonts,
        )
