"""The opening hours commands: `hours collect`, `hours set` and `hours show` (docs/COLLECTORS.md, Opening hours).

    python -m app.cli hours collect [--airport IATA ...] [--delay S] [--check]
    python -m app.cli hours set IATA --file PATH [--by USERNAME] [--source-url URL] [--check]
    python -m app.cli hours show [IATA ...]
    python -m app.cli backfill hours_seed [--check]

Registered into `app.cli` by one line, like the quality and editorial commands. `collect` reads
every airport that has a collector in `services/hours/registry.py` (or the ones named), under the
robots policy, and appends one collected row per airport read; a host that refuses is recorded
and not asked again in the same run; an airport whose pages hold no hours is left for hand
population and the run says so. `set` writes a hand row: the same shape as a collected one,
dated and named to the account that entered it, so the page never cares which it got, and a
hand row outranks every collected one after it (`services/hours/store.py`). `--check` reads or
prints and writes nothing. `backfill hours_seed` is the one-off that turns any hours constant a
written guide carries (`services/airport_guides.py`) into a hand row, safe to repeat.
"""

import argparse
import logging
from datetime import UTC, datetime
from pathlib import Path
from urllib.parse import urlsplit

from app.config import settings
from app.db import SessionLocal
from app.services.collectors.fetch import FetchError, SourceBlocked
from app.services.collectors.robots import RobotsUnavailable
from app.services.hours import base, registry, store

log = logging.getLogger(__name__)


def cmd_hours_collect(args: argparse.Namespace) -> int:
    codes = [c.upper() for c in (args.airport or [])] or registry.collected_airports()
    refused_hosts: set[str] = set()
    outcomes: list[tuple[str, str, str]] = []
    with SessionLocal() as db:
        for iata in codes:
            collector = registry.collector_for(iata)
            if collector is None:
                outcomes.append((iata, "no collector", "left for hand population (hours set)"))
                continue
            host = urlsplit(collector.homepage).netloc
            if host in refused_hosts:
                outcomes.append((iata, "skipped", f"{host} refused earlier in this run"))
                continue
            shop_ids = store.shop_ids_for(db, iata)
            if not shop_ids:
                outcomes.append((iata, "no shop", "no shops row carries this code; nothing written"))
                continue
            try:
                reading = base.collect(collector, iata, delay=args.delay)
            except SourceBlocked as exc:
                refused_hosts.add(host)
                log.warning("hours_refused iata=%s host=%s: %s", iata, host, exc)
                outcomes.append((iata, "refused", f"{exc}; left for hand population"))
                continue
            except RobotsUnavailable as exc:
                outcomes.append((iata, "error", f"{exc}"))
                continue
            except FetchError as exc:
                outcomes.append((iata, "error", f"{exc}; retry next run"))
                continue
            except base.NothingParsed as exc:
                outcomes.append((iata, "unreadable", f"{exc}; left for hand population"))
                continue
            for warning in reading.warnings:
                log.warning("hours_warning iata=%s: %s", iata, warning)
            if args.check:
                outcomes.append((iata, "read (check)", reading.text))
                continue
            row = store.record(
                db,
                shop_id=shop_ids[0],
                source_kind="collected",
                text=reading.text,
                observed_at=reading.observed_at,
                source_url=reading.source_url,
                detail=reading.detail(),
            )
            db.commit()
            outcomes.append((iata, "collected", f"row {row.id}, {len(reading.stores)} stores: {reading.text}"))
    for iata, outcome, note in outcomes:
        print(f"{iata:4} {outcome:14} {note}")
    return 0


def cmd_hours_set(args: argparse.Namespace) -> int:
    iata = args.airport.upper()
    text = " ".join(Path(args.file).read_text().split())
    if not text:
        print(f"{args.file} is empty: empty beats guessed, so nothing is written")
        return 2
    with SessionLocal() as db:
        shop_ids = store.shop_ids_for(db, iata)
        if not shop_ids:
            print(f"{iata}: no shops row carries this code; nothing written")
            return 2
        account = store.account_by_name(db, args.by)
        if account is None:
            print(f"no account named {args.by!r}: hours by hand are named to who entered them (`accounts list`)")
            return 2
        who = account.username or account.display_name
        if args.check:
            print(f"[check, nothing written] {iata} hand, by {who}: {text}")
            return 0
        row = store.record(
            db,
            shop_id=shop_ids[0],
            source_kind="hand",
            text=text,
            observed_at=datetime.now(UTC),
            source_url=args.source_url,
            entered_by_id=account.id,
            detail={"entered_by": who, "file": Path(args.file).name},
        )
        db.commit()
        print(f"{iata} hand row {row.id}, by {who} {row.observed_at.date().isoformat()}: {text}")
    return 0


def backfill_hours_seed(db) -> str:
    """A hand row from every written guide's hours constant, once. Idempotent: an airport that
    already has a hand row is left alone, and a guide with no hours seeds nothing."""
    from app.services import airport_guides

    seeded, present, empty = [], [], []
    owner = store.account_by_name(db, settings.owner_username)
    for iata, guide in sorted(airport_guides.GUIDES.items()):
        if not guide.hours:
            empty.append(iata)
            continue
        shop_ids = store.shop_ids_for(db, iata)
        if not shop_ids or store.newest(db, shop_ids, "hand") is not None:
            present.append(iata)
            continue
        store.record(
            db,
            shop_id=shop_ids[0],
            source_kind="hand",
            text=guide.hours,
            observed_at=datetime.now(UTC),
            entered_by_id=owner.id if owner else None,
            detail={"seed": "airport_guides", "entered_by": (owner.username if owner else None)},
        )
        seeded.append(iata)
    db.commit()
    return (
        f"hours_seed: seeded {len(seeded)} ({', '.join(seeded) or 'none'}); "
        f"{len(present)} already had a hand row or no shop ({', '.join(present) or 'none'}); "
        f"{len(empty)} guides carry no hours constant ({', '.join(empty) or 'none'})"
    )


def cmd_hours_show(args: argparse.Namespace) -> int:
    from sqlalchemy import select

    from app.models.catalog import Shop

    with SessionLocal() as db:
        codes = [c.upper() for c in (args.airport or [])] or sorted({
            code for code in db.scalars(select(Shop.iata).where(Shop.iata.isnot(None)).distinct())
        })
        for iata in codes:
            row = store.current_for_airport(db, iata)
            if row is None:
                print(f"{iata:4} none")
                continue
            who = store.entered_by_username(db, row)
            origin = f"entered by {who or 'unknown'}" if row.source_kind == "hand" else f"collected from {urlsplit(row.source_url or '').netloc or 'unknown'}"
            print(f"{iata:4} {row.source_kind:9} {row.observed_at.date().isoformat()} {origin}: {row.text}")
    return 0


def register(sub: argparse._SubParsersAction) -> None:
    hours = sub.add_parser("hours", help="opening hours: collect from operator sites, set by hand, show")
    hsub = hours.add_subparsers(dest="hours_command", required=True)

    collect = hsub.add_parser("collect", help="read the airports that have a collector, under robots")
    collect.add_argument("--airport", action="append", metavar="IATA", help="one airport; repeatable")
    collect.add_argument("--delay", type=float, default=base.DEFAULT_DELAY, help="seconds between requests (the host's Crawl-delay wins when slower)")
    collect.add_argument("--check", action="store_true", help="read and print; write nothing")
    collect.set_defaults(func=cmd_hours_collect)

    hand = hsub.add_parser("set", help="write an airport's hours by hand, dated and named to who entered them")
    hand.add_argument("airport", metavar="IATA")
    hand.add_argument("--file", required=True, help="a text file holding the one line the page shows")
    hand.add_argument("--by", default="rian", help="the account that entered them (username or display name; default rian)")
    hand.add_argument("--source-url", default=None, help="where the hours were read, when a page was")
    hand.add_argument("--check", action="store_true", help="print what would be written; write nothing")
    hand.set_defaults(func=cmd_hours_set)

    show = hsub.add_parser("show", help="the current hours and their provenance per airport")
    show.add_argument("airport", nargs="*", metavar="IATA")
    show.set_defaults(func=cmd_hours_show)
