#!/usr/bin/env python3
from __future__ import annotations

import argparse
import fcntl
import json
import os
import platform
import plistlib
import re
import shutil
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from typing import Any


APP_NAME = "macOS Screenshot"
APP_SLUG = "macosscreenshot"
LABEL = "com.kelownafilmstudios.macosscreenshot"
DEFAULT_SCREENSHOT_NAME = "Screen Shot"
DEFAULT_LOCATION = Path.home() / "Desktop"
SUPPORT_DIR = Path.home() / "Library" / "Application Support" / "Kelowna Film Studios" / APP_SLUG
INSTALL_PATH = SUPPORT_DIR / "macosscreenshot.py"
CONFIG_PATH = SUPPORT_DIR / "config.json"
LOCK_PATH = Path(f"/tmp/{LABEL}.lock")
LAUNCH_AGENT_PATH = Path.home() / "Library" / "LaunchAgents" / f"{LABEL}.plist"
LOG_DIR = Path.home() / "Library" / "Logs" / "Kelowna Film Studios" / APP_SLUG
SUPPORTED_EXTENSIONS = "png|jpg|jpeg|heic|tif|tiff|gif|pdf"


MODE_WATCH_24H = "watch_24h"
MODE_ONCE_24H = "once_24h"
MODE_MACOS_DEFAULT = "macos_default"
MODE_SYSTEM_24H = "system_24h"

TIME_OPTIONS = {
    "1": (MODE_WATCH_24H, "24-hour filenames, keep Mac clock unchanged"),
    "2": (MODE_ONCE_24H, "Convert existing screenshots once"),
    "3": (MODE_MACOS_DEFAULT, "Keep macOS default naming"),
    "4": (MODE_SYSTEM_24H, "Set Mac clock to 24-hour system-wide. Affects all apps."),
}


def run_command(args: list[str], check: bool = False, quiet: bool = True) -> subprocess.CompletedProcess[str]:
    kwargs: dict[str, Any] = {
        "text": True,
        "check": check,
    }
    if quiet:
        kwargs["stdout"] = subprocess.PIPE
        kwargs["stderr"] = subprocess.PIPE
    return subprocess.run(args, **kwargs)


def is_macos() -> bool:
    return platform.system() == "Darwin"


def require_macos() -> None:
    if not is_macos():
        raise SystemExit(f"{APP_NAME} is for macOS only.")


def defaults_read(domain: str, key: str) -> str | None:
    try:
        result = run_command(["defaults", "read", domain, key], check=True)
    except subprocess.CalledProcessError:
        return None
    value = result.stdout.strip() if result.stdout else ""
    return value or None


def defaults_write(domain: str, key: str, value: str) -> None:
    run_command(["defaults", "write", domain, key, value], check=True)


def defaults_write_bool(domain: str, key: str, value: bool) -> None:
    run_command(["defaults", "write", domain, key, "-bool", "true" if value else "false"], check=True)


def restart_system_ui() -> None:
    run_command(["killall", "SystemUIServer"], quiet=True)


def current_screenshot_name() -> str:
    return defaults_read("com.apple.screencapture", "name") or DEFAULT_SCREENSHOT_NAME


def current_screenshot_location() -> Path:
    location = defaults_read("com.apple.screencapture", "location")
    if not location:
        return DEFAULT_LOCATION
    return Path(location).expanduser()


def current_system_time_summary() -> str:
    forced = defaults_read("NSGlobalDomain", "AppleICUForce24HourTime")
    if forced and forced.strip().lower() in {"1", "true", "yes"}:
        return "system 24-hour"
    return "system default (12-hour/24-hour, based on Settings)"


def shorten_path(path: Path) -> str:
    try:
        relative = path.expanduser().resolve().relative_to(Path.home().resolve())
    except ValueError:
        text = str(path.expanduser())
    else:
        text = "~" if str(relative) == "." else f"~/{relative}"
    return text if text.endswith("/") else f"{text}/"


def load_config() -> dict[str, Any]:
    if not CONFIG_PATH.exists():
        return {}
    try:
        return json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return {}


def save_config(config: dict[str, Any]) -> None:
    SUPPORT_DIR.mkdir(parents=True, exist_ok=True)
    CONFIG_PATH.write_text(json.dumps(config, indent=2, sort_keys=True) + "\n", encoding="utf-8")


def configured_mode(config: dict[str, Any]) -> str:
    mode = str(config.get("mode") or "")
    if mode in {MODE_WATCH_24H, MODE_ONCE_24H, MODE_MACOS_DEFAULT, MODE_SYSTEM_24H}:
        return mode
    return MODE_MACOS_DEFAULT


def time_summary(config: dict[str, Any]) -> str:
    mode = configured_mode(config)
    if mode == MODE_WATCH_24H:
        return "24-hour filenames, Mac clock unchanged"
    if mode == MODE_ONCE_24H:
        return "24-hour cleanup once, no watcher"
    if mode == MODE_SYSTEM_24H:
        return "system-wide 24-hour clock"
    return current_system_time_summary()


def prompt_text(label: str, current: str) -> str:
    response = input(f"{label} [{current}]: ").strip()
    return response or current


def choose_time_mode(config: dict[str, Any]) -> str:
    mode_to_option = {mode: option for option, (mode, _) in TIME_OPTIONS.items()}
    default_option = mode_to_option.get(configured_mode(config), "1")

    print("\nTime:")
    for option, (_, label) in TIME_OPTIONS.items():
        suffix = " (recommended)" if option == "1" else ""
        print(f"  {option}. {label}{suffix}")

    while True:
        response = input(f"Choose [{default_option}]: ").strip() or default_option
        if response in TIME_OPTIONS:
            return TIME_OPTIONS[response][0]
        print("Choose 1, 2, 3, or 4.")


def expand_location(text: str) -> Path:
    path = Path(text).expanduser()
    if not path.is_absolute():
        path = Path.cwd() / path
    return path.resolve()


def ensure_location(path: Path) -> None:
    if path.exists() and not path.is_dir():
        raise SystemExit(f"Location is not a folder: {path}")
    if path.exists():
        return

    response = input(f"Create {shorten_path(path)}? [Y/n]: ").strip().lower()
    if response in {"", "y", "yes"}:
        path.mkdir(parents=True, exist_ok=True)
        return
    raise SystemExit("Install cancelled.")


def screenshot_regex(name: str) -> re.Pattern[str]:
    return re.compile(
        rf"^(?P<prefix>{re.escape(name)} "
        r"(?P<date>\d{4}-\d{2}-\d{2}) at )"
        r"(?P<hour>\d{1,2})\.(?P<minute>\d{2})\.(?P<second>\d{2})"
        r"[\s\u00a0\u202f]*(?P<ampm>AM|PM)"
        r"(?P<duplicate>(?: \d+| \(\d+\)))?"
        rf"\.(?P<ext>{SUPPORTED_EXTENSIONS})$",
        re.IGNORECASE,
    )


def hour_24(hour_text: str, ampm: str) -> int:
    hour = int(hour_text, 10)
    ampm = ampm.upper()
    if ampm == "AM" and hour == 12:
        return 0
    if ampm == "PM" and hour != 12:
        return hour + 12
    return hour


def destination_for(path: Path, match: re.Match[str]) -> Path | None:
    hour = hour_24(match.group("hour"), match.group("ampm"))
    duplicate = match.group("duplicate")
    duplicate_suffix = ""
    if duplicate:
        duplicate_number = re.search(r"\d+", duplicate)
        if duplicate_number:
            duplicate_suffix = f"-{int(duplicate_number.group(0), 10):02d}"

    stem = (
        f"{match.group('prefix')}"
        f"{hour:02d}.{match.group('minute')}.{match.group('second')}"
        f"{duplicate_suffix}"
    )
    ext = match.group("ext").lower()
    base = path.with_name(f"{stem}.{ext}")

    if path == base:
        return None

    if not base.exists():
        return base

    try:
        if path.samefile(base):
            return None
    except FileNotFoundError:
        return None

    for index in range(2, 1000):
        candidate = path.with_name(f"{stem}-{index:02d}.{ext}")
        if not candidate.exists():
            return candidate

    raise RuntimeError(f"Could not find a free filename for {path.name}")


def rename_matching_files(folder: Path, name: str) -> int:
    folder.mkdir(parents=True, exist_ok=True)
    pattern = screenshot_regex(name)
    renamed = 0

    for path in sorted(folder.iterdir(), key=lambda item: item.name):
        if not path.is_file():
            continue

        match = pattern.match(path.name)
        if not match:
            continue

        destination = destination_for(path, match)
        if destination is None:
            continue

        path.rename(destination)
        renamed += 1
        print(f"{path.name} -> {destination.name}", flush=True)

    return renamed


def apply_screenshot_preferences(name: str, location: Path) -> None:
    defaults_write("com.apple.screencapture", "name", name)
    defaults_write("com.apple.screencapture", "location", str(location))
    restart_system_ui()


def installed_python() -> str:
    system_python = Path("/usr/bin/python3")
    if system_python.exists():
        return str(system_python)
    return sys.executable


def install_self() -> None:
    SUPPORT_DIR.mkdir(parents=True, exist_ok=True)
    source = Path(__file__).resolve()
    if source != INSTALL_PATH:
        shutil.copy2(source, INSTALL_PATH)
    INSTALL_PATH.chmod(0o755)


def launch_agent_plist(location: Path) -> dict[str, Any]:
    LOG_DIR.mkdir(parents=True, exist_ok=True)
    return {
        "Label": LABEL,
        "ProgramArguments": [installed_python(), str(INSTALL_PATH), "--watcher"],
        "RunAtLoad": True,
        "WatchPaths": [str(location)],
        "ProcessType": "Background",
        "StandardOutPath": str(LOG_DIR / "watcher.log"),
        "StandardErrorPath": str(LOG_DIR / "watcher-error.log"),
    }


def write_launch_agent(location: Path) -> None:
    LAUNCH_AGENT_PATH.parent.mkdir(parents=True, exist_ok=True)
    plist = launch_agent_plist(location)
    with LAUNCH_AGENT_PATH.open("wb") as handle:
        plistlib.dump(plist, handle)


def unload_launch_agent() -> None:
    uid = str(os.getuid())
    if LAUNCH_AGENT_PATH.exists():
        run_command(["launchctl", "bootout", f"gui/{uid}", str(LAUNCH_AGENT_PATH)], quiet=True)
    run_command(["launchctl", "bootout", f"gui/{uid}/{LABEL}"], quiet=True)


def load_launch_agent() -> None:
    uid = str(os.getuid())
    unload_launch_agent()
    run_command(["launchctl", "bootstrap", f"gui/{uid}", str(LAUNCH_AGENT_PATH)], check=True)
    run_command(["launchctl", "enable", f"gui/{uid}/{LABEL}"], quiet=True)
    run_command(["launchctl", "kickstart", "-k", f"gui/{uid}/{LABEL}"], quiet=True)


def remove_launch_agent_file() -> None:
    unload_launch_agent()
    try:
        LAUNCH_AGENT_PATH.unlink()
    except FileNotFoundError:
        pass


def watcher_main() -> int:
    config = load_config()
    if configured_mode(config) != MODE_WATCH_24H:
        return 0

    name = str(config.get("name") or DEFAULT_SCREENSHOT_NAME)
    location = expand_location(str(config.get("location") or DEFAULT_LOCATION))

    with LOCK_PATH.open("w") as lock_file:
        try:
            fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
        except BlockingIOError:
            return 0

        renamed = rename_matching_files(location, name)
        time.sleep(0.3)
        renamed += rename_matching_files(location, name)

    return 0 if renamed >= 0 else 1


def print_current_summary(config: dict[str, Any]) -> None:
    name = str(config.get("name") or current_screenshot_name())
    location = expand_location(str(config.get("location") or current_screenshot_location()))

    print(f"\n{APP_NAME}")
    print("Current setup:")
    print(f"Name: {name}")
    print(f"Location: {shorten_path(location)}")
    print(f"Time: {time_summary(config)}")


def interactive_install() -> int:
    require_macos()
    config = load_config()
    print_current_summary(config)
    print("\nPress Return to keep a value.\n")

    current_name = str(config.get("name") or current_screenshot_name())
    current_location = expand_location(str(config.get("location") or current_screenshot_location()))

    name = prompt_text("Name", current_name)
    location_text = prompt_text("Location", shorten_path(current_location))
    location = expand_location(location_text)
    mode = choose_time_mode(config)

    if mode == MODE_SYSTEM_24H:
        print("\nThis changes the clock format everywhere on your Mac.")
        confirm = input("Continue? [y/N]: ").strip().lower()
        if confirm not in {"y", "yes"}:
            raise SystemExit("Install cancelled.")
        defaults_write_bool("NSGlobalDomain", "AppleICUForce24HourTime", True)

    ensure_location(location)
    apply_screenshot_preferences(name, location)
    install_self()

    new_config = {
        "name": name,
        "location": str(location),
        "mode": mode,
        "label": LABEL,
        "installed_script": str(INSTALL_PATH),
        "launch_agent": str(LAUNCH_AGENT_PATH),
    }
    save_config(new_config)

    if mode == MODE_WATCH_24H:
        write_launch_agent(location)
        load_launch_agent()
        renamed = rename_matching_files(location, name)
        print(f"\nInstalled watcher. Cleaned up {renamed} existing screenshot name(s).")
    elif mode == MODE_ONCE_24H:
        remove_launch_agent_file()
        renamed = rename_matching_files(location, name)
        print(f"\nConverted {renamed} existing screenshot name(s). No watcher installed.")
    else:
        remove_launch_agent_file()
        print("\nSaved screenshot settings. No rename watcher installed.")

    print("\nDone.")
    print(f"Name: {name}")
    print(f"Location: {shorten_path(location)}")
    print(f"Time: {time_summary(new_config)}")
    return 0


def status_main() -> int:
    require_macos()
    print_current_summary(load_config())
    print(f"Installed script: {INSTALL_PATH}")
    print(f"LaunchAgent: {LAUNCH_AGENT_PATH}")
    return 0


def uninstall_main() -> int:
    require_macos()
    remove_launch_agent_file()
    print(f"Removed watcher LaunchAgent: {LAUNCH_AGENT_PATH}")
    print(f"Settings and script remain in: {SUPPORT_DIR}")
    return 0


def self_test() -> int:
    with tempfile.TemporaryDirectory() as temp_dir:
        folder = Path(temp_dir)
        cases = {
            "Screen Shot 2026-06-15 at 11.30.12\u202fPM.png": "Screen Shot 2026-06-15 at 23.30.12.png",
            "Screen Shot 2026-06-15 at 12.00.01 AM.png": "Screen Shot 2026-06-15 at 00.00.01.png",
            "Screen Shot 2026-06-15 at 12.00.01 PM.png": "Screen Shot 2026-06-15 at 12.00.01.png",
            "Screen Shot 2026-06-15 at 1.02.03\u00a0AM (2).PNG": "Screen Shot 2026-06-15 at 01.02.03-02.png",
        }
        for source_name in cases:
            (folder / source_name).write_text("test", encoding="utf-8")

        renamed = rename_matching_files(folder, DEFAULT_SCREENSHOT_NAME)
        missing = [target for target in cases.values() if not (folder / target).exists()]

        if renamed != len(cases) or missing:
            print(f"Self-test failed. Renamed={renamed}, missing={missing}", file=sys.stderr)
            return 1

    print("Self-test passed.")
    return 0


def main() -> int:
    parser = argparse.ArgumentParser(description="Install or run the macOS Screenshot watcher.")
    parser.add_argument("--watcher", action="store_true", help="Run the background rename pass.")
    parser.add_argument("--status", action="store_true", help="Show the current saved setup.")
    parser.add_argument("--uninstall", action="store_true", help="Remove the watcher LaunchAgent.")
    parser.add_argument("--self-test", action="store_true", help="Run local filename conversion tests.")
    args = parser.parse_args()

    if args.self_test:
        return self_test()
    if args.watcher:
        require_macos()
        return watcher_main()
    if args.status:
        return status_main()
    if args.uninstall:
        return uninstall_main()
    return interactive_install()


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except KeyboardInterrupt:
        print("\nCancelled.")
        raise SystemExit(130)
    except Exception as exc:
        print(f"{APP_NAME} failed: {exc}", file=sys.stderr)
        raise
