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

import argparse
import os
import platform
import shutil
import subprocess
import sys
from datetime import datetime
from pathlib import Path

SCRIPT_DIR = Path(__file__).resolve().parent
DEFAULT_INIT_SOURCE = SCRIPT_DIR / "emacs-modern-init.el"
DEFAULT_TARGET_INIT = Path.home() / ".emacs.d" / "init.el"
DEFAULT_LOCAL_FILE = Path.home() / ".emacs.d" / "local.el"
DEFAULT_PYTHON_VENV = Path.home() / ".emacs.d" / "python-tools"

PACKAGE_SETS = {
    "apt": ["emacs", "git", "ripgrep", "python3-pip", "python3-venv", "nodejs", "npm", "default-jre", "plantuml"],
    "dnf": ["emacs", "git", "ripgrep", "python3-pip", "nodejs", "npm", "java-latest-openjdk", "plantuml"],
    "pacman": ["emacs", "git", "ripgrep", "python", "python-pip", "nodejs", "npm", "jre-openjdk", "plantuml"],
    "brew": ["emacs", "git", "ripgrep", "python", "node", "plantuml"],
}

CHECK_COMMANDS = [
    ["emacs", "--version"],
    ["git", "--version"],
    ["rg", "--version"],
    ["python3", "--version"],
    ["node", "--version"],
    ["npm", "--version"],
    ["java", "-version"],
    ["plantuml", "-version"],
    ["codex", "--version"],
]

PYTHON_TOOLS = ["basedpyright", "ruff"]
NPM_TOOLS = ["@openai/codex"]


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Install Emacs prerequisites and write an init.el from the public HAIDOX example.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=f"""examples:
  ./install-emacs.sh --check
  ./install-emacs.sh --apply
  ./install-emacs.sh --apply --skip-system-packages
  ./install-emacs.sh --apply --target ~/.emacs.d/init.el
  ./install-emacs.sh --apply --setup-python-venv
  ./install-emacs.sh --apply --setup-python-venv --install-codex

what this does:
  1. detects a package manager: apt, dnf, pacman, or brew
  2. optionally installs Emacs and common helper tools
  3. backs up an existing init.el
  4. copies the selected init source to the target init.el
  5. checks the resulting Emacs setup in batch mode

safety:
  Default mode is --check and does not change files or install packages.
  Use --apply to install packages or write init.el.
  System package installation may ask for sudo depending on your OS.
""",
    )
    mode = parser.add_mutually_exclusive_group()
    mode.add_argument("--check", action="store_true", help="Show detected tools and planned actions without changing anything. Default.")
    mode.add_argument("--apply", action="store_true", help="Install requested packages and write init.el.")
    parser.add_argument("--init-source", default=str(DEFAULT_INIT_SOURCE), help="Source init.el example to copy.")
    parser.add_argument("--target", default=str(DEFAULT_TARGET_INIT), help="Target init.el path.")
    parser.add_argument("--skip-system-packages", action="store_true", help="Do not install OS packages.")
    parser.add_argument("--install-python-tools", action="store_true", help="Install basedpyright and ruff with python3 -m pip --user.")
    parser.add_argument("--setup-python-venv", action="store_true", help="Create a Python venv for editor tools and install basedpyright and ruff there.")
    parser.add_argument("--python-venv", default=str(DEFAULT_PYTHON_VENV), help="Python venv path used with --setup-python-venv.")
    parser.add_argument("--local-file", default=str(DEFAULT_LOCAL_FILE), help="Emacs local.el path updated with the venv PATH block.")
    parser.add_argument("--skip-local-file", action="store_true", help="Do not update local.el when --setup-python-venv is used.")
    parser.add_argument("--install-codex", action="store_true", help="Install @openai/codex with npm -g when npm is available.")
    parser.add_argument("--package-manager", choices=sorted(PACKAGE_SETS), help="Override package manager detection.")
    parser.add_argument("--no-backup", action="store_true", help="Replace target init.el without making a timestamped backup.")
    parser.add_argument("--no-verify", action="store_true", help="Skip emacs --batch verification after writing init.el.")
    return parser.parse_args()


def quote(command: list[str]) -> str:
    return " ".join(shlex_quote(part) for part in command)


def shlex_quote(value: str) -> str:
    if not value:
        return "''"
    safe = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_@%+=:,./-"
    if all(char in safe for char in value):
        return value
    return "'" + value.replace("'", "'\\''") + "'"


def run(command: list[str], *, apply: bool, check: bool = True) -> subprocess.CompletedProcess[str] | None:
    print("$", quote(command))
    if not apply:
        return None
    return subprocess.run(command, text=True, check=check)


def run_output(command: list[str], *, check: bool = False) -> subprocess.CompletedProcess[str]:
    return subprocess.run(command, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=check)


def command_exists(name: str) -> bool:
    return shutil.which(name) is not None


def detect_package_manager(override: str | None = None) -> str | None:
    if override:
        return override
    for candidate in ("apt", "dnf", "pacman", "brew"):
        if command_exists(candidate):
            return candidate
    return None


def install_command(manager: str, packages: list[str]) -> list[str]:
    if manager == "apt":
        base = ["apt", "install", "-y", *packages]
        return base if os.geteuid() == 0 else ["sudo", *base]
    if manager == "dnf":
        base = ["dnf", "install", "-y", *packages]
        return base if os.geteuid() == 0 else ["sudo", *base]
    if manager == "pacman":
        base = ["pacman", "-S", "--needed", "--noconfirm", *packages]
        return base if os.geteuid() == 0 else ["sudo", *base]
    if manager == "brew":
        return ["brew", "install", *packages]
    raise ValueError(f"unsupported package manager: {manager}")


def package_refresh_command(manager: str) -> list[str] | None:
    if manager == "apt":
        base = ["apt", "update"]
        return base if os.geteuid() == 0 else ["sudo", *base]
    if manager == "dnf":
        return ["dnf", "makecache"] if os.geteuid() == 0 else ["sudo", "dnf", "makecache"]
    if manager == "pacman":
        return ["pacman", "-Sy"] if os.geteuid() == 0 else ["sudo", "pacman", "-Sy"]
    return None


def print_status() -> None:
    print(f"system: {platform.system()} {platform.release()}")
    for command in CHECK_COMMANDS:
        executable = command[0]
        path = shutil.which(executable)
        label = " ".join(command)
        if not path:
            print(f"missing: {label}")
            continue
        print(f"found: {executable} -> {path}")


def backup_target(target: Path, *, apply: bool, no_backup: bool) -> None:
    if not target.exists() or no_backup:
        return
    stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
    backup = target.with_name(f"{target.name}.backup-{stamp}")
    print(f"backup: {target} -> {backup}")
    if apply:
        shutil.copy2(target, backup)


def write_init(source: Path, target: Path, *, apply: bool, no_backup: bool) -> None:
    if not source.is_file():
        raise SystemExit(f"init source not found: {source}")
    print(f"init source: {source}")
    print(f"init target: {target}")
    backup_target(target, apply=apply, no_backup=no_backup)
    if apply:
        target.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(source, target)
        print(f"wrote: {target}")
    else:
        print("planned: copy init source to target")


def python_venv_bin(venv: Path) -> Path:
    return venv / ("Scripts" if platform.system() == "Windows" else "bin")


def python_venv_python(venv: Path) -> Path:
    executable = "python.exe" if platform.system() == "Windows" else "python"
    return python_venv_bin(venv) / executable


def managed_local_block(venv: Path) -> str:
    venv_bin = python_venv_bin(venv)
    return "\n".join(
        [
            ";; HAIDOX-EMACS-PYTHON-TOOLS:BEGIN",
            ";; This block lets Emacs find Python tools installed in the editor venv.",
            f"(let ((venv-bin (expand-file-name {elisp_string(venv_bin)})))",
            "  (when (file-directory-p venv-bin)",
            "    (setenv \"PATH\" (concat venv-bin path-separator (getenv \"PATH\")))",
            "    (add-to-list 'exec-path venv-bin)))",
            ";; HAIDOX-EMACS-PYTHON-TOOLS:END",
            "",
        ]
    )


def replace_managed_block(existing: str, block: str) -> str:
    begin = ";; HAIDOX-EMACS-PYTHON-TOOLS:BEGIN"
    end = ";; HAIDOX-EMACS-PYTHON-TOOLS:END"
    start = existing.find(begin)
    finish = existing.find(end)
    if start != -1 and finish != -1 and start < finish:
        finish = existing.find("\n", finish)
        finish = len(existing) if finish == -1 else finish + 1
        return existing[:start].rstrip() + "\n\n" + block + existing[finish:].lstrip()
    return existing.rstrip() + "\n\n" + block if existing.strip() else block


def write_local_file(local_file: Path, venv: Path, *, apply: bool) -> None:
    block = managed_local_block(venv)
    print(f"local file: {local_file}")
    print(f"managed venv PATH: {python_venv_bin(venv)}")
    if not apply:
        print("planned: update local.el managed Python tools block")
        return
    local_file.parent.mkdir(parents=True, exist_ok=True)
    existing = local_file.read_text(encoding="utf-8") if local_file.exists() else ""
    local_file.write_text(replace_managed_block(existing, block), encoding="utf-8")
    print(f"wrote: {local_file}")


def setup_python_venv(venv: Path, local_file: Path, *, apply: bool, skip_local_file: bool) -> None:
    print(f"python venv: {venv}")
    check_venv = run_output(["python3", "-m", "venv", "--help"])
    if check_venv.returncode != 0:
        print("warning: python3 -m venv is not available")
        print("hint: install python3-venv on Debian/Ubuntu, then run this command again")
        if apply:
            raise SystemExit(1)
    run(["python3", "-m", "venv", str(venv)], apply=apply)
    venv_python = python_venv_python(venv)
    run([str(venv_python), "-m", "pip", "install", "--upgrade", "pip", *PYTHON_TOOLS], apply=apply)
    if skip_local_file:
        print("skip local.el update: --skip-local-file")
    else:
        write_local_file(local_file, venv, apply=apply)


def elisp_string(value: Path) -> str:
    return '"' + str(value).replace('\\', '\\\\').replace('"', '\\"') + '"'


def verify_emacs(target: Path, *, apply: bool) -> None:
    if not command_exists("emacs"):
        print("skip verify: emacs command not found")
        return
    eval_form = (
        "(with-temp-buffer "
        f"(insert-file-contents {elisp_string(target)}) "
        "(emacs-lisp-mode) "
        "(check-parens) "
        "(message \"init syntax ok\"))"
    )
    command = ["emacs", "--batch", "-Q", "--eval", eval_form]
    run(command, apply=apply)


def main() -> int:
    args = parse_args()
    apply = bool(args.apply)
    print_status()

    manager = detect_package_manager(args.package_manager)
    print(f"package manager: {manager or 'not found'}")
    if not args.skip_system_packages:
        if manager is None:
            print("skip packages: no supported package manager found")
        else:
            packages = PACKAGE_SETS[manager]
            refresh = package_refresh_command(manager)
            if refresh:
                run(refresh, apply=apply)
            run(install_command(manager, packages), apply=apply)
    else:
        print("skip packages: --skip-system-packages")

    if args.setup_python_venv:
        setup_python_venv(
            Path(args.python_venv).expanduser(),
            Path(args.local_file).expanduser(),
            apply=apply,
            skip_local_file=args.skip_local_file,
        )
    elif args.install_python_tools:
        run(["python3", "-m", "pip", "install", "--user", "--upgrade", *PYTHON_TOOLS], apply=apply)
    else:
        print("skip python tools: add --setup-python-venv or --install-python-tools")

    if args.install_codex:
        run(["npm", "install", "-g", *NPM_TOOLS], apply=apply)
    else:
        print("skip Codex CLI install: add --install-codex to install @openai/codex")

    source = Path(args.init_source).expanduser().resolve()
    target = Path(args.target).expanduser()
    write_init(source, target, apply=apply, no_backup=args.no_backup)

    if not args.no_verify:
        verify_emacs(target, apply=apply)
    else:
        print("skip verify: --no-verify")

    if not apply:
        print("check complete: re-run with --apply to make changes")
    return 0


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