from __future__ import annotations

import os
import signal
import subprocess
import sys
import tarfile
import time
from datetime import datetime, timezone
from pathlib import Path

import click

from sitedrop.server.auth import hash_password
from sitedrop.server.config import DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_PATH, ServerConfig

PID_FILE = DEFAULT_CONFIG_DIR / "server.pid"
LOG_FILE = DEFAULT_CONFIG_DIR / "server.log"
LOG_MAX_BYTES = 5 * 1024 * 1024
LOG_BACKUP = DEFAULT_CONFIG_DIR / "server.log.1"


def _read_pid() -> int | None:
    """Read PID from file, returning None if missing or corrupt."""
    if not PID_FILE.exists():
        return None
    try:
        return int(PID_FILE.read_text().strip())
    except (ValueError, OSError):
        PID_FILE.unlink(missing_ok=True)
        return None


def _is_running(pid: int) -> bool:
    """Check if a process with the given PID is alive."""
    try:
        os.kill(pid, 0)
        return True
    except OSError:
        return False


def _is_sitedrop_process(pid: int) -> bool:
    """Check if the given PID belongs to a sitedrop/uvicorn process."""
    try:
        os.kill(pid, 0)
    except OSError:
        return False
    try:
        cmdline = open(f"/proc/{pid}/cmdline").read()
        return "uvicorn" in cmdline and "sitedrop" in cmdline
    except (OSError, FileNotFoundError):
        # /proc not available (macOS/BSD) — fall back to trusting the PID
        return True


def _rotate_log_if_needed() -> None:
    try:
        if LOG_FILE.exists() and LOG_FILE.stat().st_size > LOG_MAX_BYTES:
            LOG_FILE.replace(LOG_BACKUP)
    except OSError:
        pass


@click.command()
@click.option("--dev", is_flag=True, help="Configure for local development")
def setup(dev: bool):
    """Set up the server (set password, configure hosting)."""
    config = ServerConfig.load()

    password = click.prompt(
        "Set admin password", hide_input=True, confirmation_prompt=True
    )
    config.password_hash = hash_password(password)

    if dev:
        config.host = "127.0.0.1"
        config.port = 8000
        click.echo("Configured for local development on 127.0.0.1:8000")
    else:
        config.host = click.prompt("Bind host", default=config.host)
        config.port = click.prompt("Bind port", default=config.port, type=int)

    config.save()
    click.echo(f"Config saved to {DEFAULT_CONFIG_DIR / 'server.json'}")


@click.command()
@click.option("--dev", is_flag=True, help="Run with uvicorn directly (foreground)")
@click.option("--ssl-certfile", default=None, help="Path to SSL certificate file")
@click.option("--ssl-keyfile", default=None, help="Path to SSL key file")
def start(dev: bool, ssl_certfile: str | None, ssl_keyfile: str | None):
    """Start the sitedrop server."""
    config = ServerConfig.load()

    if not config.password_hash:
        click.echo("Error: run 'sitedrop setup' first to set a password.", err=True)
        raise SystemExit(1)

    if (ssl_certfile is None) != (ssl_keyfile is None):
        click.echo(
            "Error: --ssl-certfile and --ssl-keyfile must be provided together.",
            err=True,
        )
        raise SystemExit(1)

    if ssl_certfile and ssl_keyfile:
        config.ssl_certfile = str(Path(ssl_certfile).resolve())
        config.ssl_keyfile = str(Path(ssl_keyfile).resolve())
        config.save()

    if dev:
        click.echo(f"Starting dev server on {config.host}:{config.port}")
        import uvicorn

        kwargs = {
            "host": config.host,
            "port": config.port,
            "reload": True,
        }
        if config.ssl_certfile and config.ssl_keyfile:
            kwargs["ssl_certfile"] = config.ssl_certfile
            kwargs["ssl_keyfile"] = config.ssl_keyfile
        uvicorn.run("sitedrop.server.app:create_app", factory=True, **kwargs)
    else:
        pid = _read_pid()
        if pid is not None and _is_sitedrop_process(pid):
            click.echo(f"Server already running (PID {pid})")
            return

        click.echo(f"Starting server on {config.host}:{config.port}")
        DEFAULT_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
        _rotate_log_if_needed()
        log_fh = open(LOG_FILE, "a")
        cmd = [
            sys.executable,
            "-m",
            "uvicorn",
            "sitedrop.server.app:create_app",
            "--factory",
            "--host",
            config.host,
            "--port",
            str(config.port),
        ]
        if config.ssl_certfile and config.ssl_keyfile:
            cmd.extend(["--ssl-certfile", config.ssl_certfile])
            cmd.extend(["--ssl-keyfile", config.ssl_keyfile])
        proc = subprocess.Popen(
            cmd,
            stdout=log_fh,
            stderr=log_fh,
            start_new_session=True,
        )
        time.sleep(0.5)
        if proc.poll() is not None:
            log_fh.close()
            click.echo("Error: server failed to start.", err=True)
            click.echo(f"Check logs at {LOG_FILE}", err=True)
            raise SystemExit(1)
        log_fh.close()
        PID_FILE.write_text(str(proc.pid))
        click.echo(f"Server started (PID {proc.pid})")
        click.echo(f"Logs: {LOG_FILE}")


@click.command()
def stop():
    """Stop the sitedrop server."""
    pid = _read_pid()
    if pid is None:
        click.echo("Server is not running.")
        return

    if not _is_sitedrop_process(pid):
        click.echo("Server is not running (stale PID file).")
        PID_FILE.unlink(missing_ok=True)
        return

    try:
        os.kill(pid, signal.SIGTERM)
    except OSError:
        click.echo("Server was not running.")
        PID_FILE.unlink(missing_ok=True)
        return

    for _ in range(10):
        time.sleep(0.5)
        if not _is_running(pid):
            click.echo(f"Stopped server (PID {pid})")
            PID_FILE.unlink(missing_ok=True)
            return

    try:
        os.kill(pid, signal.SIGKILL)
        click.echo(f"Force-killed server (PID {pid})")
    except OSError:
        click.echo(f"Stopped server (PID {pid})")
    PID_FILE.unlink(missing_ok=True)


@click.command()
@click.option("--dev", is_flag=True, help="Run with uvicorn directly (foreground)")
@click.pass_context
def restart(ctx, dev: bool):
    """Restart the sitedrop server."""
    ctx.invoke(stop)
    ctx.invoke(start, dev=dev)


@click.command()
def status():
    """Check if the server is running."""
    pid = _read_pid()
    if pid is None:
        click.echo("Server is not running.")
        return

    if _is_sitedrop_process(pid):
        config = ServerConfig.load()
        click.echo(f"Server is running (PID {pid}) on {config.host}:{config.port}")
    else:
        click.echo("Server is not running (stale PID file).")
        PID_FILE.unlink(missing_ok=True)


@click.command()
@click.option("-f", "--follow", is_flag=True, help="Follow log output in real time")
@click.option("-n", "--lines", default=20, type=int, help="Number of lines to show")
def logs(follow: bool, lines: int):
    """View server logs."""
    if not LOG_FILE.exists():
        click.echo("No log file found.")
        return

    content = LOG_FILE.read_text()
    all_lines = content.splitlines()
    for line in all_lines[-lines:]:
        click.echo(line)

    if follow:
        try:
            with open(LOG_FILE) as f:
                f.seek(0, 2)
                while True:
                    line = f.readline()
                    if line:
                        click.echo(line, nl=False)
                    else:
                        time.sleep(0.1)
        except KeyboardInterrupt:
            pass


@click.command()
@click.option(
    "-o", "--output", default=None, type=click.Path(), help="Output file path"
)
def backup(output: str | None):
    """Back up server config and all sites to a .tar.gz archive."""
    if not DEFAULT_CONFIG_PATH.exists():
        click.echo(
            "Error: no server config found. Run 'sitedrop setup' first.", err=True
        )
        raise SystemExit(1)

    config = ServerConfig.load()
    sites_dir = Path(config.sites_dir)

    if output is None:
        ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
        output = f"sitedrop-backup-{ts}.tar.gz"

    with tarfile.open(output, "w:gz") as tar:
        tar.add(str(DEFAULT_CONFIG_PATH), arcname="server.json")
        if sites_dir.exists():
            for html_file in sorted(sites_dir.glob("*.html")):
                tar.add(str(html_file), arcname=f"sites/{html_file.name}")

    click.echo(f"Backup saved to {output}")


@click.command()
@click.argument("file", type=click.Path(exists=True, path_type=Path))
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompts")
def restore(file: Path, yes: bool):
    """Restore server config and sites from a backup archive."""
    try:
        tar = tarfile.open(file, "r:gz")
    except (tarfile.TarError, OSError) as e:
        click.echo(f"Error: could not open archive: {e}", err=True)
        raise SystemExit(1)

    with tar:
        # Security: reject path traversal
        for member in tar.getmembers():
            if member.name.startswith("/") or ".." in member.name:
                click.echo(
                    f"Error: archive contains unsafe path: {member.name}", err=True
                )
                raise SystemExit(1)

        # Warn if server is running
        pid = _read_pid()
        if pid is not None and _is_sitedrop_process(pid):
            if not yes:
                click.echo(f"Warning: server is running (PID {pid}).")
                if not click.confirm("Continue with restore?"):
                    click.echo("Aborted.")
                    return

        # Show contents and confirm
        names = [m.name for m in tar.getmembers()]
        if not yes:
            click.echo("Archive contents:")
            for name in names:
                click.echo(f"  {name}")
            if not click.confirm("Restore these files?"):
                click.echo("Aborted.")
                return

        # Extract safely (manual writes, not extractall)
        config = ServerConfig.load()
        sites_dir = Path(config.sites_dir)

        for member in tar.getmembers():
            f = tar.extractfile(member)
            if f is None:
                continue
            data = f.read()

            if member.name == "server.json":
                DEFAULT_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
                DEFAULT_CONFIG_PATH.write_bytes(data)
                os.chmod(DEFAULT_CONFIG_PATH, 0o600)
            elif member.name.startswith("sites/"):
                sites_dir.mkdir(parents=True, exist_ok=True)
                dest = sites_dir / Path(member.name).name
                dest.write_bytes(data)

    click.echo("Restore complete.")
