"""Typer CLI commands — all user-facing commands."""

from __future__ import annotations

import os
import re
from typing import Optional

import typer
from typing_extensions import Annotated

from lockin import __version__
from lockin.config import Config, Profile, Schedule, load_config, save_config
from lockin.presets import PRESETS, get_preset, list_presets
from lockin.session import create_session, get_active_session, load_session, SESSION_FILE
from lockin.apps import list_installed_apps
from lockin.daemon import install_daemon, uninstall_daemon, is_daemon_installed
from lockin.blocker import apply_blocks, remove_blocks
from lockin.ui import (
    console,
    print_error,
    print_info,
    print_success,
    print_warning,
    show_always_blocked,
    show_apps,
    show_presets,
    show_profile_detail,
    show_profiles,
    show_schedules,
    show_status,
)

app = typer.Typer(
    name="lockin",
    help="CLI focus blocker for macOS — block distracting websites and apps.",
    no_args_is_help=False,
    invoke_without_command=True,
)

profile_app = typer.Typer(help="Manage blocking profiles.")
schedule_app = typer.Typer(help="Manage auto-start schedules.")
app.add_typer(profile_app, name="profile")
app.add_typer(schedule_app, name="schedule")


def _parse_duration(duration_str: str) -> int:
    """Parse duration string like '2h', '30m', '1h30m', '90s' into seconds."""
    pattern = r"(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?"
    match = re.fullmatch(pattern, duration_str.strip())
    if not match or not any(match.groups()):
        raise typer.BadParameter(
            f"Invalid duration '{duration_str}'. Use format like: 2h, 30m, 1h30m, 90s"
        )
    hours = int(match.group(1) or 0)
    minutes = int(match.group(2) or 0)
    seconds = int(match.group(3) or 0)
    total = hours * 3600 + minutes * 60 + seconds
    if total <= 0:
        raise typer.BadParameter("Duration must be greater than 0.")
    return total


def _require_root(action: str = "This action") -> None:
    if os.geteuid() != 0:
        print_error(f"{action} requires root privileges. Run with sudo.")
        raise typer.Exit(1)


# -- Main command (status) --


@app.callback()
def main_callback(
    ctx: typer.Context,
    version: Annotated[
        bool, typer.Option("--version", "-v", help="Show version and exit.")
    ] = False,
) -> None:
    """Lockin — CLI focus blocker for macOS."""
    if version:
        console.print(f"lockin v{__version__}")
        raise typer.Exit()
    # If no subcommand given, show status
    if ctx.invoked_subcommand is None:
        session = load_session()
        if session and session.verify() and not session.is_expired:
            show_status(session)
        else:
            show_status(None)


@app.command()
def status() -> None:
    """Show current session status."""
    session = load_session()
    if session and session.verify() and not session.is_expired:
        show_status(session)
    else:
        show_status(None)


# -- Start / Stop --


@app.command()
def start(
    profile_name: Annotated[str, typer.Argument(help="Profile name to activate.")],
    duration: Annotated[
        str, typer.Option("--duration", "-d", help="Session duration (e.g. 2h, 30m, 1h30m).")
    ] = "1h",
) -> None:
    """Start a focus session."""
    _require_root("Starting a focus session")

    # Check for existing active session
    active = get_active_session()
    if active:
        print_error(
            f"A session is already active (profile: {active.profile_name}, "
            f"remaining: {int(active.remaining_seconds)}s). Cannot start another."
        )
        raise typer.Exit(1)

    config = load_config()
    profile = config.profiles.get(profile_name)
    if profile is None:
        print_error(f"Profile '{profile_name}' not found. Create one with: lockin profile create {profile_name} --preset social")
        raise typer.Exit(1)

    duration_seconds = _parse_duration(duration)
    blocked_domains = profile.resolve_domains()
    blocked_apps = profile.resolve_apps()

    # Add always-blocked items
    from lockin.presets import SUBDOMAIN_PREFIXES

    for site in config.always_blocked.sites:
        for prefix in SUBDOMAIN_PREFIXES:
            d = f"{prefix}{site}"
            if d not in blocked_domains:
                blocked_domains.append(d)
    for a in config.always_blocked.apps:
        if a not in blocked_apps:
            blocked_apps.append(a)

    if not blocked_domains and not blocked_apps:
        print_warning("This profile has nothing to block. Add presets or custom sites first.")
        raise typer.Exit(1)

    # Check if daemon is installed
    if not is_daemon_installed():
        print_warning("Watchdog daemon is not installed. Installing now...")
        if not install_daemon():
            print_error("Failed to install watchdog daemon.")
            raise typer.Exit(1)
        print_success("Watchdog daemon installed.")

    # Apply blocks
    if blocked_domains:
        if not apply_blocks(blocked_domains):
            print_error("Failed to apply website blocks. Are you running as root?")
            raise typer.Exit(1)

    # Kill blocked apps immediately
    from lockin.apps import kill_blocked_apps

    killed = kill_blocked_apps(blocked_apps)
    if killed:
        print_info(f"Killed blocked apps: {', '.join(killed)}")

    # Create signed session
    sess = create_session(
        profile_name=profile_name,
        duration_seconds=duration_seconds,
        blocked_domains=blocked_domains,
        blocked_apps=blocked_apps,
    )

    from lockin.ui import format_duration

    print_success(f"Focus session started!")
    print_info(f"Profile: {profile_name}")
    print_info(f"Duration: {format_duration(duration_seconds)}")
    print_info(f"Blocking: {len(blocked_domains)} domains, {len(blocked_apps)} apps")
    print_warning("This session cannot be stopped until the timer expires.")


@app.command()
def stop(
    force: Annotated[
        bool, typer.Option("--force", "-f", help="Force stop (NOT ALLOWED during active sessions).")
    ] = False,
) -> None:
    """Stop an active session. REFUSED during active sessions — this is by design."""
    session = load_session()

    if session is None:
        print_info("No active session to stop.")
        return

    if session.verify() and not session.is_expired:
        # Active valid session — refuse to stop
        from lockin.ui import format_duration

        remaining = session.remaining_seconds
        print_error("Cannot stop an active focus session. This is by design.")
        print_error(f"Remaining: {format_duration(remaining)}")
        print_warning("The session will end automatically when the timer expires.")
        if force:
            print_error("Even --force cannot stop an active session. Stay focused!")
        raise typer.Exit(1)

    # Session is expired or invalid — clean up
    print_info("Session has expired. Cleaning up...")
    remove_blocks()
    from lockin.session import delete_session

    delete_session()
    print_success("Session cleaned up.")


# -- Presets --


@app.command("preset")
def preset_list() -> None:
    """Show built-in presets."""
    show_presets(list_presets())


# -- Profile management --


@profile_app.command("list")
def profile_list() -> None:
    """List all profiles."""
    config = load_config()
    show_profiles(config.profiles)


@profile_app.command("create")
def profile_create(
    name: Annotated[str, typer.Argument(help="Profile name.")],
    preset: Annotated[
        Optional[list[str]],
        typer.Option("--preset", "-p", help="Preset category to include (can repeat)."),
    ] = None,
    site: Annotated[
        Optional[list[str]],
        typer.Option("--site", "-s", help="Custom domain to block (can repeat)."),
    ] = None,
    block_app: Annotated[
        Optional[list[str]],
        typer.Option("--app", "-a", help="App name to block (can repeat)."),
    ] = None,
) -> None:
    """Create a new blocking profile."""
    config = load_config()

    if name in config.profiles:
        print_error(f"Profile '{name}' already exists. Delete it first or choose another name.")
        raise typer.Exit(1)

    presets = preset or []
    for p in presets:
        if p not in PRESETS:
            print_error(f"Unknown preset '{p}'. Available: {', '.join(PRESETS.keys())}")
            raise typer.Exit(1)

    profile = Profile(
        name=name,
        presets=presets,
        custom_sites=site or [],
        blocked_apps=block_app or [],
    )
    config.profiles[name] = profile
    save_config(config)

    print_success(f"Profile '{name}' created.")
    show_profile_detail(profile)


@profile_app.command("show")
def profile_show(
    name: Annotated[str, typer.Argument(help="Profile name to show.")],
) -> None:
    """Show details of a profile."""
    config = load_config()
    profile = config.profiles.get(name)
    if profile is None:
        print_error(f"Profile '{name}' not found.")
        raise typer.Exit(1)
    show_profile_detail(profile)


@profile_app.command("update")
def profile_update(
    name: Annotated[str, typer.Argument(help="Profile name to update.")],
    add_preset: Annotated[
        Optional[list[str]],
        typer.Option("--add-preset", help="Add a preset (can repeat)."),
    ] = None,
    remove_preset: Annotated[
        Optional[list[str]],
        typer.Option("--remove-preset", help="Remove a preset (can repeat)."),
    ] = None,
    add_site: Annotated[
        Optional[list[str]],
        typer.Option("--add-site", help="Add a custom domain (can repeat)."),
    ] = None,
    remove_site: Annotated[
        Optional[list[str]],
        typer.Option("--remove-site", help="Remove a custom domain (can repeat)."),
    ] = None,
    add_app: Annotated[
        Optional[list[str]],
        typer.Option("--add-app", help="Add an app to block (can repeat)."),
    ] = None,
    remove_app: Annotated[
        Optional[list[str]],
        typer.Option("--remove-app", help="Remove a blocked app (can repeat)."),
    ] = None,
) -> None:
    """Update an existing profile — add or remove presets, sites, and apps."""
    config = load_config()
    profile = config.profiles.get(name)
    if profile is None:
        print_error(f"Profile '{name}' not found.")
        raise typer.Exit(1)

    for p in (add_preset or []):
        if p not in PRESETS:
            print_error(f"Unknown preset '{p}'. Available: {', '.join(PRESETS.keys())}")
            raise typer.Exit(1)
        if p not in profile.presets:
            profile.presets.append(p)
            print_success(f"Added preset '{p}'.")
        else:
            print_warning(f"Preset '{p}' already in profile.")

    for p in (remove_preset or []):
        if p in profile.presets:
            profile.presets.remove(p)
            print_success(f"Removed preset '{p}'.")
        else:
            print_warning(f"Preset '{p}' not in profile.")

    for s in (add_site or []):
        if s not in profile.custom_sites:
            profile.custom_sites.append(s)
            print_success(f"Added site '{s}'.")
        else:
            print_warning(f"Site '{s}' already in profile.")

    for s in (remove_site or []):
        if s in profile.custom_sites:
            profile.custom_sites.remove(s)
            print_success(f"Removed site '{s}'.")
        else:
            print_warning(f"Site '{s}' not in profile.")

    for a in (add_app or []):
        if a not in profile.blocked_apps:
            profile.blocked_apps.append(a)
            print_success(f"Added app '{a}'.")
        else:
            print_warning(f"App '{a}' already in profile.")

    for a in (remove_app or []):
        if a in profile.blocked_apps:
            profile.blocked_apps.remove(a)
            print_success(f"Removed app '{a}'.")
        else:
            print_warning(f"App '{a}' not in profile.")

    save_config(config)
    show_profile_detail(profile)


@profile_app.command("delete")
def profile_delete(
    name: Annotated[str, typer.Argument(help="Profile name to delete.")],
) -> None:
    """Delete a profile."""
    # Block deletion if the profile is in use by an active session
    active = get_active_session()
    if active and active.profile_name == name:
        print_error(f"Cannot delete profile '{name}' — it's in use by the active session.")
        raise typer.Exit(1)

    config = load_config()
    if name not in config.profiles:
        print_error(f"Profile '{name}' not found.")
        raise typer.Exit(1)
    del config.profiles[name]
    save_config(config)
    print_success(f"Profile '{name}' deleted.")


# -- Always-blocked --


@app.command("block")
def block_domain(
    domain: Annotated[str, typer.Argument(help="Domain to always block.")],
) -> None:
    """Add a domain to the always-blocked list."""
    config = load_config()
    if domain in config.always_blocked.sites:
        print_warning(f"'{domain}' is already in the always-blocked list.")
        return
    config.always_blocked.sites.append(domain)
    save_config(config)
    print_success(f"Added '{domain}' to always-blocked list.")


@app.command("unblock")
def unblock_domain(
    domain: Annotated[str, typer.Argument(help="Domain to remove from always-blocked.")],
) -> None:
    """Remove a domain from the always-blocked list."""
    config = load_config()
    if domain not in config.always_blocked.sites:
        print_error(f"'{domain}' is not in the always-blocked list.")
        raise typer.Exit(1)
    config.always_blocked.sites.remove(domain)
    save_config(config)
    print_success(f"Removed '{domain}' from always-blocked list.")


# -- Schedules --


@schedule_app.command("list")
def schedule_list() -> None:
    """List all schedules."""
    config = load_config()
    show_schedules(config.schedules)


@schedule_app.command("create")
def schedule_create(
    name: Annotated[str, typer.Argument(help="Schedule name.")],
    profile_name: Annotated[str, typer.Option("--profile", "-p", help="Profile to activate.")],
    days: Annotated[str, typer.Option("--days", "-D", help="Comma-separated days (mon,tue,wed,thu,fri,sat,sun).")],
    start_time: Annotated[str, typer.Option("--start", "-s", help="Start time (HH:MM).")] = "09:00",
    duration_minutes: Annotated[int, typer.Option("--duration", "-d", help="Duration in minutes.")] = 120,
) -> None:
    """Create an auto-start schedule."""
    config = load_config()

    if name in config.schedules:
        print_error(f"Schedule '{name}' already exists.")
        raise typer.Exit(1)

    if profile_name not in config.profiles:
        print_error(f"Profile '{profile_name}' not found. Create it first.")
        raise typer.Exit(1)

    day_map = {
        "mon": "Monday", "tue": "Tuesday", "wed": "Wednesday",
        "thu": "Thursday", "fri": "Friday", "sat": "Saturday", "sun": "Sunday",
    }
    day_list: list[str] = []
    for d in days.split(","):
        d = d.strip().lower()
        if d not in day_map:
            print_error(f"Invalid day '{d}'. Use: mon,tue,wed,thu,fri,sat,sun")
            raise typer.Exit(1)
        day_list.append(day_map[d])

    schedule = Schedule(
        name=name,
        profile=profile_name,
        days=day_list,
        start_time=start_time,
        duration_minutes=duration_minutes,
    )
    config.schedules[name] = schedule
    save_config(config)
    print_success(f"Schedule '{name}' created.")


@schedule_app.command("delete")
def schedule_delete(
    name: Annotated[str, typer.Argument(help="Schedule name to delete.")],
) -> None:
    """Delete a schedule."""
    config = load_config()
    if name not in config.schedules:
        print_error(f"Schedule '{name}' not found.")
        raise typer.Exit(1)
    del config.schedules[name]
    save_config(config)
    print_success(f"Schedule '{name}' deleted.")


# -- Apps --


@app.command("apps")
def apps_list() -> None:
    """List detected macOS apps."""
    installed = list_installed_apps()
    show_apps(installed)


# -- Daemon install --


@app.command("install")
def install_cmd() -> None:
    """Install the launchd watchdog daemon."""
    _require_root("Installing the daemon")
    if install_daemon():
        print_success("Watchdog daemon installed and loaded.")
        print_info("The daemon will enforce focus sessions and prevent tampering.")
    else:
        print_error("Failed to install the watchdog daemon.")
        raise typer.Exit(1)


@app.command("uninstall")
def uninstall_cmd() -> None:
    """Uninstall the launchd watchdog daemon."""
    _require_root("Uninstalling the daemon")

    # Refuse if there's an active session
    active = get_active_session()
    if active:
        print_error("Cannot uninstall while a focus session is active.")
        raise typer.Exit(1)

    if uninstall_daemon():
        print_success("Watchdog daemon uninstalled.")
    else:
        print_error("Failed to uninstall the watchdog daemon.")
        raise typer.Exit(1)


def main() -> None:
    app()
