#!/usr/bin/env python3

"""Generate a user launch daemon plist for macOS to run the dev server."""

import argparse
from collections import OrderedDict
from dataclasses import dataclass
import json
import os
from pathlib import Path
import plistlib
import shutil
import subprocess


macos_root = Path(__file__).resolve().parent
repo_root = macos_root.parent.parent
run_sh = macos_root / "run.sh"
main_scpt = macos_root / "main.applescript"


@dataclass
class Env:
    name: str
    destination: str
    devport: int
    appname: str
    appid: str
    svclabel: str


constants = {
    "DevPort": 64224,
    "VedPort": 42246,
}
env_dev_obverse = Env(
    name="dev-obverse",
    destination="public/dev-obverse",
    devport=64224,
    appname="me.micahrl.com.dev-obverse.app",
    appid="com.micahrl.me.dev-obverse",
    svclabel="com.micahrl.me.dev-obverse.background",
)
env_dev_reverse = Env(
    name="dev-reverse",
    destination="public/dev-reverse",
    devport=42246,
    appname="me.micahrl.com.dev-reverse.app",
    appid="com.micahrl.me.dev-reverse",
    svclabel="com.micahrl.me.dev-reverse.background",
)


def get_tailscale_hostname():
    """Get the Tailscale hostname for the current machine."""
    try:
        result = subprocess.run(
            ["/Applications/Tailscale.app/Contents/MacOS/Tailscale", "status", "--json"],
            capture_output=True,
            text=True,
            check=True,
        )
        status = json.loads(result.stdout)
        return status["Self"]["DNSName"].rstrip(".")
    except (subprocess.CalledProcessError, KeyError, FileNotFoundError):
        return None


def build_app_bundle(bundlepath: str, hugo: str, devhost: str, env: Env, reporoot: str, force: bool = False):
    """Build the app bundle for the dev server."""
    if os.path.exists(bundlepath):
        if force:
            print(f"Removing existing app bundle at {bundlepath}")
            shutil.rmtree(bundlepath)
        else:
            raise FileExistsError(f"App bundle already exists at {bundlepath}. Use --force to overwrite.")
    Path(bundlepath).parent.mkdir(parents=True, exist_ok=True)

    # Build the app bundle
    subprocess.run(["osacompile", "-o", bundlepath, main_scpt.as_posix()], check=True)

    plist_path = Path(bundlepath) / "Contents" / "Info.plist"
    with open(plist_path, "rb") as f:
        plist_data = plistlib.load(f, dict_type=OrderedDict)

    # Set the app to an "agent" which does not show in the dock or menu bar
    plist_data["LSUIElement"] = True
    # Set bundle identifiers and names
    plist_data["CFBundleName"] = env.appname
    # This shows in System Settings > Security & Privacy > Privacy > Full Disk Access, IF the bundle ID is set below
    # Otherwise the app will just be shown as `applet`
    plist_data["CFBundleDisplayName"] = env.appname
    # Set bundle ID
    plist_data["CFBundleIdentifier"] = env.appid
    # Write the envirohnment configuration to the app bundle too, so that the run.sh script can read it directly
    plist_data["MicahrlHugo"] = hugo
    plist_data["MicahrlEnvironment"] = env.name
    plist_data["MicahrlDestination"] = env.destination
    plist_data["MicahrlServiceHost"] = devhost
    plist_data["MicahrlServicePort"] = env.devport
    plist_data["MicahrlDevPort"] = constants["DevPort"]
    plist_data["MicahrlVedPort"] = constants["VedPort"]

    # Write the plist data back to the Info.plist
    with open(plist_path, "wb") as f:
        plistlib.dump(plist_data, f, sort_keys=False)

    # Embed the resources into the app bundle

    # The run script
    bundle_run_sh = Path(bundlepath) / "Contents" / "Resources" / "run.sh"
    shutil.copy(run_sh, bundle_run_sh)
    bundle_run_sh.chmod(0o755)

    # Copy and customize the launchd plist
    bundle_launchd_plist = Path(bundlepath) / "Contents" / "Resources" / "launchd.plist"
    with open(macos_root / "launchd.plist", "rb") as f:
        launchd_data = plistlib.load(f, dict_type=OrderedDict)

    # Set the actual values in the launchd plist
    launchd_data["Label"] = env.svclabel
    launchd_data["Program"] = str(Path(bundlepath).resolve() / "Contents" / "MacOS" / "applet")

    # Set the environment variables
    log_path = os.path.expanduser(f"~/Library/Logs/{env.svclabel}.log")
    launchd_data["EnvironmentVariables"]["MICAHRL_REPOROOT"] = reporoot
    launchd_data["EnvironmentVariables"]["MICAHRL_LOG_PATH"] = log_path

    # Write the customized launchd plist
    with open(bundle_launchd_plist, "wb") as f:
        plistlib.dump(launchd_data, f, sort_keys=False)

    # Code sign the whole thing
    subprocess.run(["codesign", "--deep", "--force", "--sign", "-", bundlepath], check=True)


def main():
    parser = argparse.ArgumentParser(description="Generate a launch daemon plist for macOS to run the dev server.")
    parser.add_argument("environment", choices=["dev-obverse", "dev-reverse"], help="The environment to run the dev server for")
    parser.add_argument("appbundle", type=Path)
    parser.add_argument("reporoot", type=Path, help="Path to the repository root")
    parser.add_argument("--force", action="store_true", help="Force overwrite the app bundle if it exists")
    devhost_group = parser.add_mutually_exclusive_group(required=False)
    devhost_group.add_argument("--devhost", default="localhost", help="The dev server host (default: %(default)s)")
    devhost_group.add_argument(
        "--devhost-tailscale", action="store_true", help="Use Tailscale to determine the dev server host"
    )
    parser.add_argument("--hugo", help="Path to the Hugo binary (default: found in PATH)")
    parsed = parser.parse_args()

    if parsed.devhost_tailscale:
        devhost = get_tailscale_hostname()
        if not devhost:
            parser.error("Error: Could not determine Tailscale hostname.")
    else:
        devhost = parsed.devhost

    hugo = parsed.hugo or shutil.which("hugo")
    if not hugo:
        parser.error("Error: Hugo not found. Please specify --hugo or install Hugo to your $PATH.")

    if parsed.environment == "dev-obverse":
        env = env_dev_obverse
    elif parsed.environment == "dev-reverse":
        env = env_dev_reverse
    else:
        parser.error(f"Unknown environment: {parsed.environment}")

    build_app_bundle(parsed.appbundle, hugo, devhost, env, str(parsed.reporoot.resolve()), force=parsed.force)


if __name__ == "__main__":
    main()
