from pathlib import Path
from dataclasses import dataclass
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import json
import os
import shutil
import sys
import time

TIMEZONE = ZoneInfo("Pacific/Auckland")


def now() -> datetime:
    return datetime.now(TIMEZONE)


DEFAULT_VIDEO_EXTENSIONS = tuple(
    ext.strip().lower()
    for ext in os.getenv("VIDEO_EXTENSIONS", ".mp4,.mkv,.mov,.avi,.webm").split(",")
    if ext.strip()
)


@dataclass(frozen=True)
class PrerollConfig:
    active_dir: Path
    inactive_dir: Path
    state_file: Path
    rotate_weekday: int = 0
    schedule_time: str = "02:00"
    video_extensions: tuple[str, ...] = DEFAULT_VIDEO_EXTENSIONS


def config_from_env() -> PrerollConfig:
    return PrerollConfig(
        active_dir=Path(os.getenv("ACTIVE_DIR", os.getenv("PREROLL_ACTIVE_DIR", "/media/Prerolls"))),
        inactive_dir=Path(os.getenv("INACTIVE_DIR", os.getenv("PREROLL_INACTIVE_DIR", "/media/Prerolls - Not Active"))),
        state_file=Path(os.getenv("STATE_FILE", os.getenv("PREROLL_STATE_FILE", "cache/preroll-state.json"))),
        rotate_weekday=int(os.getenv("ROTATE_WEEKDAY", os.getenv("PREROLL_WEEKDAY", "0"))),
        schedule_time=os.getenv("SCHEDULE_TIME", os.getenv("PREROLL_TIME", "02:00")).strip(),
    )


RUN_MODE = os.getenv("RUN_MODE", "schedule").strip().lower()  # "schedule" or "once"


def log(message: str) -> None:
    print(f"[{now().isoformat(timespec='seconds')}] {message}", flush=True)


def get_videos(folder: Path, video_extensions: tuple[str, ...]):
    if not folder.exists():
        raise FileNotFoundError(f"Folder does not exist: {folder}")

    return sorted(
        [
            f for f in folder.iterdir()
            if f.is_file() and f.suffix.lower() in video_extensions
        ],
        key=lambda x: x.name.lower(),
    )


def current_week_key() -> str:
    # ISO year + ISO week is safer around New Year than %Y-%W.
    iso = now().isocalendar()
    return f"{iso.year}-W{iso.week:02d}"


def should_rotate(config: PrerollConfig, *, quiet: bool = False) -> bool:
    today = now()

    if today.weekday() != config.rotate_weekday:
        if not quiet:
            log(f"Today is weekday {today.weekday()}; rotation weekday is {config.rotate_weekday}. No rotation.")
        return False

    week_key = current_week_key()

    if not config.state_file.exists():
        if not quiet:
            log("No previous rotation state found. Rotation allowed.")
        return True

    try:
        state = json.loads(config.state_file.read_text(encoding="utf-8"))
        last_week = state.get("last_week")
        if last_week != week_key:
            if not quiet:
                log(f"Last rotation was {last_week}; current week is {week_key}. Rotation allowed.")
            return True
        if not quiet:
            log(f"Already rotated for {week_key}. No rotation.")
        return False
    except Exception as exc:
        if not quiet:
            log(f"Could not read state file: {exc}. Rotation allowed.")
        return True


def read_rotation_state(config: PrerollConfig) -> dict:
    if not config.state_file.exists():
        return {}
    try:
        state = json.loads(config.state_file.read_text(encoding="utf-8"))
    except Exception:
        return {}
    return {k: state.get(k) for k in ("last_week", "last_rotation", "active_file")}


def save_rotation(config: PrerollConfig, active_name: str) -> dict:
    config.state_file.parent.mkdir(parents=True, exist_ok=True)
    payload = {
        "last_week": current_week_key(),
        "last_rotation": now().isoformat(timespec="seconds"),
        "active_file": active_name,
    }
    config.state_file.write_text(
        json.dumps(
            payload,
            indent=2,
        ),
        encoding="utf-8",
    )
    return payload


def move_file(src: Path, dst_dir: Path) -> Path:
    dst = dst_dir / src.name
    if dst.exists():
        raise FileExistsError(f"Destination already exists: {dst}")
    shutil.move(str(src), str(dst))
    return dst


def rotate(config: PrerollConfig) -> str | None:
    config.active_dir.mkdir(parents=True, exist_ok=True)
    config.inactive_dir.mkdir(parents=True, exist_ok=True)

    active_files = get_videos(config.active_dir, config.video_extensions)
    inactive_files = get_videos(config.inactive_dir, config.video_extensions)

    if len(active_files) > 1:
        log("More than one active preroll found. Moving extras to inactive.")
        for extra in active_files[1:]:
            move_file(extra, config.inactive_dir)

    active_files = get_videos(config.active_dir, config.video_extensions)
    inactive_files = get_videos(config.inactive_dir, config.video_extensions)

    current_active = active_files[0] if active_files else None
    all_files = sorted(active_files + inactive_files, key=lambda x: x.name.lower())

    if not all_files:
        log("No preroll videos found.")
        return None

    if len(all_files) == 1:
        only_file = all_files[0]
        if only_file.parent != config.active_dir:
            moved = move_file(only_file, config.active_dir)
            log(f"Only one preroll exists. Activated: {moved.name}")
            return moved.name
        log(f"Only one preroll exists and is already active: {only_file.name}")
        return only_file.name

    if current_active:
        current_index = next(
            i for i, f in enumerate(all_files)
            if f.name.lower() == current_active.name.lower()
        )
        next_file = all_files[(current_index + 1) % len(all_files)]
    else:
        next_file = all_files[0]

    if current_active:
        moved_out = move_file(current_active, config.inactive_dir)
        log(f"Moved current active to inactive: {moved_out.name}")

    moved_in = move_file(next_file, config.active_dir)
    log(f"Activated new preroll: {moved_in.name}")
    return moved_in.name


def run_rotation(config: PrerollConfig, *, force: bool = False) -> dict:
    try:
        if not force and not should_rotate(config):
            state = read_rotation_state(config)
            return {
                "ok": True,
                "rotated": False,
                "message": "Rotation not due yet.",
                "state": state,
            }
        active_name = rotate(config)
        if not active_name:
            return {
                "ok": True,
                "rotated": False,
                "message": "No preroll videos found.",
                "state": read_rotation_state(config),
            }
        state = save_rotation(config, active_name)
        return {
            "ok": True,
            "rotated": True,
            "message": f"Activated {active_name}",
            "active_file": active_name,
            "state": state,
        }
    except Exception as exc:
        log(f"ERROR: {exc}")
        return {
            "ok": False,
            "rotated": False,
            "message": str(exc),
            "state": read_rotation_state(config),
        }


def run_once(config: PrerollConfig | None = None) -> int:
    result = run_rotation(config or config_from_env())
    return 0 if result["ok"] else 1


def parse_schedule_time(value: str) -> tuple[int, int]:
    hh, mm = value.split(":", 1)
    hour = int(hh)
    minute = int(mm)
    if not (0 <= hour < 24 and 0 <= minute < 60):
        raise ValueError(f"SCHEDULE_TIME out of range: {value}")
    return hour, minute


def next_run_after(config: PrerollConfig, base: datetime | None = None) -> datetime:
    current = base or now()
    hour, minute = parse_schedule_time(config.schedule_time)
    days_ahead = (config.rotate_weekday - current.weekday()) % 7
    target = current.replace(hour=hour, minute=minute, second=0, microsecond=0) + timedelta(days=days_ahead)
    if days_ahead == 0 and target <= current:
        target += timedelta(days=7)
    return target


def seconds_until_next(config: PrerollConfig) -> float:
    target = next_run_after(config)
    return (target - now()).total_seconds()


def run_scheduled(config: PrerollConfig | None = None) -> int:
    config = config or config_from_env()
    try:
        parse_schedule_time(config.schedule_time)
    except Exception as exc:
        log(f"ERROR: invalid SCHEDULE_TIME '{config.schedule_time}': {exc}")
        return 1

    log(f"Scheduler started. Weekly run on weekday {config.rotate_weekday} at {config.schedule_time} local time.")
    while True:
        delay = seconds_until_next(config)
        next_run = now() + timedelta(seconds=delay)
        log(f"Sleeping {int(delay)}s until next run at {next_run.isoformat(timespec='seconds')}.")
        time.sleep(delay)
        log("Scheduled run triggered.")
        run_rotation(config)


def main() -> int:
    config = config_from_env()
    log("Starting preroll rotator.")
    log(f"Active folder: {config.active_dir}")
    log(f"Inactive folder: {config.inactive_dir}")
    log(f"Run mode: {RUN_MODE}")

    if RUN_MODE == "once":
        return run_once(config)
    return run_scheduled(config)


if __name__ == "__main__":
    sys.exit(main())
