"""Main entrypoint for jprov."""

from __future__ import annotations

import argparse
import os
import sys
import time
from datetime import datetime, timezone

from .config import ConfigError, jail_conf_path, load_jail_config, load_main_config, resolve_overlay_path, validate_configs
from .jailconf import render_jail_conf
from .mounts import mount_all, unmount_all
from .overlay import copy_overlay
from .provision import run_provision
from .runner import log_line, run
from .zfs import clone_dataset, dataset_exists, destroy_dataset, get_mountpoint


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(prog="jprov", description="FreeBSD jail provisioning tool")
    parser.add_argument("-y", "--yes", action="store_true", help="skip confirmations")
    parser.add_argument("--dry-run", action="store_true", help="log actions without executing")

    subparsers = parser.add_subparsers(dest="command", required=True)
    up = subparsers.add_parser("up", help="provision a jail (fails if it already exists)")
    up.add_argument("jailname", help="name of the jail to provision")

    down = subparsers.add_parser("down", help="destroy a jail only")
    down.add_argument("jailname", help="name of the jail to destroy")
    return parser


def confirm(prompt: str) -> bool:
    try:
        return input(f"{prompt} [y/N]: ").strip().lower() in {"y", "yes"}
    except EOFError:
        return False


def jail_exists(name: str, *, log_file: str | None = None) -> bool:
    result = run(["jls", "-j", name], allow_fail=True, log_file=log_file)
    return result.returncode == 0 and result.stdout.strip() != ""


def destroy_jail(
    *,
    jailname: str,
    jail_cfg,
    jail_ds: str,
    log_file: str | None,
    dry_run: bool,
    yes: bool,
) -> None:
    exists = dataset_exists(jail_ds, log_file=log_file, dry_run=dry_run) or jail_exists(
        jailname, log_file=log_file
    )
    if not exists:
        log_line("INFO", "destroy", f"jail {jailname} does not exist")
        return

    if not yes:
        if not confirm(f"Jail {jailname} exists and will be destroyed. Continue?"):
            log_line("INFO", "confirm", "aborted by user")
            raise RuntimeError("aborted by user")

    log_line("INFO", "destroy", f"stopping jail {jailname}")
    run(["service", "jail", "onestop", jailname], log_file=log_file, allow_fail=True, dry_run=dry_run)
    root = get_mountpoint(jail_ds, log_file=log_file) if dataset_exists(jail_ds, log_file=log_file) else ""
    if root:
        log_line("INFO", "destroy", "unmounting volumes")
        unmount_all(root, jail_cfg.mounts, log_file=log_file)
    log_line("INFO", "destroy", f"destroying dataset {jail_ds}")
    if not dry_run:
        destroy_dataset(jail_ds, log_file=log_file)


def main(argv: list[str] | None = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)

    log_file = None
    dry_run = args.dry_run
    try:
        main_cfg = load_main_config()
        jail_cfg = load_jail_config(main_cfg, args.jailname)
        validate_configs(main_cfg, jail_cfg)

        os.makedirs(main_cfg.log_dir, exist_ok=True)
        ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
        log_file = os.path.join(main_cfg.log_dir, f"{args.jailname}-{ts}.log")

        log_line("INFO", "validate", "OK (config and paths)")
        log_line("INFO", "config", f"loaded {jail_cfg.config_path}")

        template_ds = f"{main_cfg.templates_prefix}{jail_cfg.template}"
        jail_ds = f"{main_cfg.containers_prefix}{args.jailname}"

        if args.command == "down":
            destroy_jail(
                jailname=args.jailname,
                jail_cfg=jail_cfg,
                jail_ds=jail_ds,
                log_file=log_file,
                dry_run=dry_run,
                yes=args.yes,
            )
            log_line("INFO", "done", "destroy complete")
            return 0

        if not dataset_exists(template_ds, log_file=log_file, dry_run=dry_run):
            raise RuntimeError(f"template dataset not found: {template_ds}")

        exists = dataset_exists(jail_ds, log_file=log_file, dry_run=dry_run) or jail_exists(
            args.jailname, log_file=log_file
        )
        if exists:
            raise RuntimeError(f"jail already exists: {args.jailname} (run `jprov down` first)")

        log_line("INFO", "create", f"cloning {template_ds} -> {jail_ds}")
        if not dry_run:
            clone_dataset(template_ds, jail_ds, log_file=log_file)
            root = get_mountpoint(jail_ds, log_file=log_file)
        else:
            root = f"<mountpoint:{jail_ds}>"

        os.makedirs(main_cfg.jail_conf_dir, exist_ok=True)
        conf_path = jail_conf_path(main_cfg, args.jailname)
        tmp_conf = conf_path + ".tmp"
        if not dry_run:
            with open(tmp_conf, "w", encoding="utf-8") as fh:
                fh.write(render_jail_conf(main_cfg, jail_cfg, root=root))
            os.replace(tmp_conf, conf_path)
            log_line("INFO", "jailconf", f"wrote {conf_path}")
        else:
            log_line("INFO", "jailconf", f"[dry-run] would write {conf_path}")

        log_line("INFO", "start", f"starting jail {args.jailname}")
        run(["service", "jail", "start", args.jailname], log_file=log_file, dry_run=dry_run)
        if not dry_run:
            for attempt in range(5):
                if jail_exists(args.jailname, log_file=log_file):
                    break
                time.sleep(1)
            else:
                raise RuntimeError(f"jail failed to start: {args.jailname}")

        if jail_cfg.mounts:
            log_line("INFO", "mounts", "mounting nullfs volumes")
            if not dry_run:
                mount_all(root, jail_cfg.mounts, log_file=log_file)

        overlay_path = resolve_overlay_path(main_cfg, jail_cfg)
        if overlay_path and os.path.isdir(overlay_path):
            log_line("INFO", "overlay", f"copying overlay from {overlay_path}")
            if not dry_run:
                copy_overlay(overlay_path, root)

        log_line("INFO", "provision", "running provisioning command")
        if not dry_run:
            run_provision(jail_cfg, log_file=log_file)
        else:
            log_line(
                "INFO",
                "provision",
                f"[dry-run] jexec {args.jailname} {jail_cfg.cmds['provision']}",
            )

        log_line("INFO", "done", "provisioning complete")
        return 0
    except (ConfigError, RuntimeError, OSError) as exc:
        log_line("ERROR", "fatal", str(exc))
        if log_file:
            print(f"script logs written to {log_file}")
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
