"""
Folder/filename-based date guessing and "taken_at looks wrong" detection.

Direct Python port of `frontend/src/lib/guessDateFromPath.ts` — the logic
must stay in sync because the frontend renders the suggestion hint in the
info panel while the backend owns the `has_date_warning` flag that the
filter bar queries. Any heuristic change has to be applied to both files.

The guesser walks a filepath, tries the filename first as the source of
truth, then falls back to folder segments (deepest first) and multi-
segment layouts. Returns ``None`` when no recognisable date can be
extracted. `has_date_warning()` compares the guess to a stored `taken_at`
and reports whether the difference is large enough to flag.
"""
from __future__ import annotations

import re
from dataclasses import dataclass
from datetime import datetime
from typing import Literal, Optional


Confidence = Literal["high", "medium", "low"]
Source = Literal["folder", "filename"]


@dataclass(frozen=True)
class DateGuess:
    date: datetime
    confidence: Confidence
    matched: str
    source: Source


_MIN_YEAR = 1970
# Bump the ceiling annually via `datetime.now()` rather than a literal so
# we don't ship a time bomb. `+1` allows near-future timestamps (cameras
# with a slightly advanced clock at year end) without opening the door to
# 4-digit serial numbers that happen to start with "30xx".
def _max_year() -> int:
    return datetime.now().year + 1


def _valid_year(y: int) -> bool:
    return _MIN_YEAR <= y <= _max_year()


def _make_date(y: int, m: int, d: int) -> Optional[datetime]:
    if not _valid_year(y):
        return None
    if not (1 <= m <= 12):
        return None
    if not (1 <= d <= 31):
        return None
    try:
        # Noon local so downstream day-bucketing is stable across timezone
        # rounding. The frontend mirrors this.
        return datetime(y, m, d, 12, 0, 0)
    except ValueError:
        return None


def _segments(filepath: str) -> list[str]:
    return [s for s in re.split(r"[\\/]+", filepath) if s]


_COMPACT_RE = re.compile(r"(?<!\d)(\d{4})(\d{2})(\d{2})(?!\d)")
_DASHED_RE = re.compile(r"(?<!\d)(\d{4})[-_.](\d{1,2})[-_.](\d{1,2})(?!\d)")
_MONTH_RE = re.compile(r"(?<!\d)(\d{4})[-_.](\d{1,2})(?!\d)")
_YEAR_RE = re.compile(r"(?<!\d)(\d{4})(?!\d)")
_FOUR_DIGITS = re.compile(r"^\d{4}$")
_ONE_OR_TWO = re.compile(r"^\d{1,2}$")


def _guess_from_string(
    input: str,
    source: Source,
    allow_year_only: bool,
) -> Optional[DateGuess]:
    if not input:
        return None

    m = _COMPACT_RE.search(input)
    if m:
        d = _make_date(int(m.group(1)), int(m.group(2)), int(m.group(3)))
        if d:
            return DateGuess(
                date=d,
                confidence="high",
                matched=f"{m.group(1)}-{m.group(2)}-{m.group(3)}",
                source=source,
            )

    m = _DASHED_RE.search(input)
    if m:
        d = _make_date(int(m.group(1)), int(m.group(2)), int(m.group(3)))
        if d:
            return DateGuess(
                date=d,
                confidence="high",
                matched=f"{m.group(1)}-{m.group(2)}-{m.group(3)}",
                source=source,
            )

    m = _MONTH_RE.search(input)
    if m:
        d = _make_date(int(m.group(1)), int(m.group(2)), 15)
        if d:
            return DateGuess(
                date=d,
                confidence="medium",
                matched=f"{m.group(1)}-{m.group(2)}",
                source=source,
            )

    if allow_year_only:
        m = _YEAR_RE.search(input)
        if m:
            d = _make_date(int(m.group(1)), 7, 1)
            if d:
                return DateGuess(
                    date=d,
                    confidence="low",
                    matched=m.group(1),
                    source=source,
                )

    return None


def _guess_from_folder_layout(folders: list[str]) -> Optional[DateGuess]:
    # YYYY / MM / DD
    for i in range(len(folders) - 2):
        a, b, c = folders[i], folders[i + 1], folders[i + 2]
        if _FOUR_DIGITS.match(a) and _ONE_OR_TWO.match(b) and _ONE_OR_TWO.match(c):
            d = _make_date(int(a), int(b), int(c))
            if d:
                return DateGuess(
                    date=d,
                    confidence="high",
                    matched=f"{a}/{b}/{c}",
                    source="folder",
                )
    # YYYY / MM
    for i in range(len(folders) - 1):
        a, b = folders[i], folders[i + 1]
        if _FOUR_DIGITS.match(a) and _ONE_OR_TWO.match(b):
            d = _make_date(int(a), int(b), 15)
            if d:
                return DateGuess(
                    date=d,
                    confidence="medium",
                    matched=f"{a}/{b}",
                    source="folder",
                )
    return None


_CONFIDENCE_RANK: dict[Confidence, int] = {"high": 3, "medium": 2, "low": 1}


def guess_date_from_path(filepath: str) -> Optional[DateGuess]:
    """Filename wins when it has any viable match; otherwise walk folder
    segments deepest-first and pick the strongest hit."""
    if not filepath:
        return None

    segs = _segments(filepath)
    if not segs:
        return None
    filename = segs[-1]
    folders = segs[:-1]

    from_filename = _guess_from_string(filename, "filename", allow_year_only=False)
    if from_filename:
        return from_filename

    best: Optional[DateGuess] = None
    for seg in reversed(folders):
        hit = _guess_from_string(seg, "folder", allow_year_only=True)
        if not hit:
            continue
        if not best or _CONFIDENCE_RANK[hit.confidence] > _CONFIDENCE_RANK[best.confidence]:
            best = hit
            if hit.confidence == "high":
                break

    from_layout = _guess_from_folder_layout(folders)
    if from_layout and (
        not best or _CONFIDENCE_RANK[from_layout.confidence] > _CONFIDENCE_RANK[best.confidence]
    ):
        best = from_layout

    return best


_ONE_DAY = 24 * 60 * 60


def has_date_warning(filepath: str, taken_at: Optional[datetime]) -> bool:
    """True when the path-based guess disagrees with ``taken_at`` by more
    than 24h, or when ``taken_at`` is missing and the path would supply
    one. This is the authoritative flag stored on `photos.has_date_warning`
    and queried by the timeline filter."""
    guess = guess_date_from_path(filepath)
    if not guess:
        return False
    if taken_at is None:
        return True
    try:
        diff = abs((taken_at - guess.date).total_seconds())
    except (TypeError, ValueError):
        return False
    return diff > _ONE_DAY
