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

import argparse
import hashlib
import json
import os
import plistlib
import re
import shutil
import subprocess
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from urllib.parse import urlparse


ROOT = Path(__file__).resolve().parents[1]
APP_NAME = "AI桌宠"
BUNDLE_ID = "com.local.ai-memorial-desktop-pet"
DEFAULT_RELEASE_ID = "v0.1-candidate"
PET_NAME = "周六"
ELECTRON_APP_REL = "node_modules/electron/dist/Electron.app"
DEFAULT_APPROVED_PACKAGE_REL = "outputs/approved_pet_packages/周六"
PET_ANIMATION_PACKAGE_REL = "packages/pet_animation"
APP_RESOURCE_REL = "Contents/Resources/app"
PET_PACKAGES_RESOURCE_REL = "Contents/Resources/pet-packages"
CODE_SIGNING_IDENTITY_LOOKUP = "security find-identity -v -p codesigning"
DMG_CREATE_COMMAND = "hdiutil create"
V02_DESKTOP_REQUIRED_FILES = [
    "apps/desktop/local-service.cjs",
    "apps/desktop/create-preload.cjs",
    "apps/desktop/create.html",
    "apps/desktop/package-store.cjs",
    "packages/pet_package_schema",
]
LOCAL_CREATE_REQUIRED_FILES = [
    "tests/fixtures/public",
    "scripts/run_production_local.py",
    "workers/run_pet_generation_worker.py",
]


def main() -> int:
    parser = argparse.ArgumentParser(description="Package the desktop pet as a macOS .app and DMG.")
    parser.add_argument("--pet-package", type=Path, default=ROOT / DEFAULT_APPROVED_PACKAGE_REL)
    parser.add_argument("--electron-app", type=Path, default=ROOT / ELECTRON_APP_REL)
    parser.add_argument("--release-id", default=DEFAULT_RELEASE_ID)
    parser.add_argument("--launch-mode", choices=["desktop", "create", "cloud-create"], default="desktop")
    parser.add_argument("--api-base-url", help="Remote API base URL for cloud-create desktop beta builds")
    parser.add_argument("--build-runner", choices=["demo", "external"], default="demo")
    parser.add_argument("--env-path", type=Path)
    parser.add_argument("--output-dir", type=Path)
    parser.add_argument("--sign-identity", default="auto", help="'auto', '-' for ad hoc, or an explicit codesign identity")
    args = parser.parse_args()
    output_dir = args.output_dir or ROOT / "outputs" / "installers" / args.release_id
    env_path = args.env_path.resolve() if args.env_path else None

    api_base_url = args.api_base_url or os.environ.get("SIXXIE_API_BASE_URL")
    report = package_desktop_app(
        pet_package=args.pet_package.resolve(),
        electron_app=args.electron_app.resolve(),
        output_dir=output_dir.resolve(),
        release_id=args.release_id,
        launch_mode=args.launch_mode,
        build_runner=args.build_runner,
        env_path=env_path,
        api_base_url=api_base_url,
        sign_identity=args.sign_identity,
    )
    print(json.dumps(report, ensure_ascii=False, indent=2))
    return 0 if report["ok"] else 1


def package_desktop_app(
    *,
    pet_package: Path,
    electron_app: Path,
    output_dir: Path,
    release_id: str,
    launch_mode: str,
    build_runner: str,
    env_path: Path | None,
    api_base_url: str | None,
    sign_identity: str,
) -> dict[str, Any]:
    if launch_mode != "cloud-create":
        _require_existing_dir(pet_package, "pet package")
    _require_existing_dir(electron_app, "Electron.app template")
    if launch_mode not in {"desktop", "create", "cloud-create"}:
        raise ValueError(f"unsupported launch_mode: {launch_mode}")
    if build_runner not in {"demo", "external"}:
        raise ValueError(f"unsupported build_runner: {build_runner}")
    if launch_mode == "cloud-create" and not str(api_base_url or "").strip():
        raise ValueError("--api-base-url is required for cloud-create packages")
    output_dir.mkdir(parents=True, exist_ok=True)

    staging_root = Path(tempfile.mkdtemp(prefix="ai-desktop-pet-package-", dir="/private/tmp"))
    app_path = staging_root / f"{APP_NAME}.app"
    dmg_path = output_dir / f"{APP_NAME}-{release_id}.dmg"
    report_path = output_dir / "package-report.json"
    stale_output_app_path = output_dir / f"{APP_NAME}.app"
    if stale_output_app_path.exists():
        shutil.rmtree(stale_output_app_path)
    if dmg_path.exists():
        dmg_path.unlink()

    shutil.copytree(electron_app, app_path, symlinks=True)
    _rewrite_app_identity(app_path)
    _install_desktop_app_files(
        app_path,
        launch_mode=launch_mode,
        build_runner=build_runner,
        env_path=env_path,
        api_base_url=api_base_url,
    )
    if launch_mode != "cloud-create":
        _install_pet_package(app_path, pet_package)

    selected_identity = _select_sign_identity(sign_identity)
    signing_status = "developer_id" if selected_identity["kind"] == "developer_id" else "ad_hoc"
    xattr_cleanup_result = _strip_extended_attributes(app_path)
    hardened_runtime_entitlements = _electron_entitlements_plist(staging_root)
    helper_app_signing = _codesign_electron_helper_apps(
        app_path=app_path,
        identity=selected_identity["identity"],
        entitlements=hardened_runtime_entitlements,
    )
    nested_runtime_signing = _codesign_electron_nested_runtime(
        app_path=app_path,
        identity=selected_identity["identity"],
    )
    codesign_result = _codesign_app(
        app_path=app_path,
        identity=selected_identity["identity"],
        entitlements=hardened_runtime_entitlements,
    )
    nested_runtime_team_check = _check_electron_nested_runtime_team(
        app_path=app_path,
        identity=selected_identity["identity"],
    )
    dmg_result = _create_dmg(app_path=app_path, dmg_path=dmg_path, release_id=release_id)
    dmg_sign_result = _codesign_dmg_if_possible(dmg_path=dmg_path, identity=selected_identity["identity"])

    report = {
        "ok": (
            nested_runtime_signing["ok"]
            and helper_app_signing["ok"]
            and codesign_result["ok"]
            and nested_runtime_team_check["ok"]
            and dmg_result["ok"]
            and dmg_sign_result["ok"]
        ),
        "release": release_id,
        "app_name": APP_NAME,
        "bundle_id": BUNDLE_ID,
        "launch_mode": launch_mode,
        "build_runner": build_runner,
        "desktop_env_path_configured": env_path is not None,
        "api_base_url_configured": bool(api_base_url),
        "api_base_url_host": _api_base_url_host(api_base_url),
        "local_service_disabled": launch_mode == "cloud-create",
        "app_path": str(app_path),
        "dmg_path": str(dmg_path),
        "staging_root": str(staging_root),
        "packaged_app_delivery": "inside_dmg",
        "pet_package": None if launch_mode == "cloud-create" else str(pet_package),
        "signing_status": signing_status,
        "signing_identity": selected_identity["display"],
        "developer_id_available": selected_identity["kind"] == "developer_id",
        "xattr_cleanup": xattr_cleanup_result,
        "hardened_runtime_entitlements": str(hardened_runtime_entitlements),
        "helper_app_signing": helper_app_signing,
        "nested_runtime_signing": nested_runtime_signing,
        "codesign": codesign_result,
        "nested_runtime_team_check": nested_runtime_team_check,
        "dmg": dmg_result,
        "dmg_sign": dmg_sign_result,
        "artifacts": {
            "dmg_sha256": _sha256_file(dmg_path) if dmg_path.exists() else None,
            "pet_json_sha256": None if launch_mode == "cloud-create" else _sha256_file(pet_package / "pet.json"),
            "spritesheet_sha256": None if launch_mode == "cloud-create" else _sha256_file(pet_package / "spritesheet.png"),
        },
        "created_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
        "distribution_note": (
            "Developer ID signed and ready for notarization."
            if selected_identity["kind"] == "developer_id"
            else "Ad-hoc signed local test build; not trusted by Gatekeeper for external distribution."
        ),
    }
    report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    return report


def _rewrite_app_identity(app_path: Path) -> None:
    info_path = app_path / "Contents" / "Info.plist"
    info = plistlib.loads(info_path.read_bytes())
    info["CFBundleDisplayName"] = APP_NAME
    info["CFBundleName"] = APP_NAME
    info["CFBundleIdentifier"] = BUNDLE_ID
    info["CFBundleShortVersionString"] = "0.1.0"
    info["CFBundleVersion"] = "0.1.0"
    info["LSApplicationCategoryType"] = "public.app-category.lifestyle"
    info.pop("ElectronAsarIntegrity", None)
    info_path.write_bytes(plistlib.dumps(info, sort_keys=False))


def _install_desktop_app_files(
    app_path: Path,
    *,
    launch_mode: str,
    build_runner: str,
    env_path: Path | None,
    api_base_url: str | None,
) -> None:
    resources = app_path / "Contents" / "Resources"
    default_asar = resources / "default_app.asar"
    if default_asar.exists():
        default_asar.unlink()
    app_dir = app_path / APP_RESOURCE_REL
    if app_dir.exists():
        shutil.rmtree(app_dir)
    (app_dir / "apps").mkdir(parents=True)
    (app_dir / "packages").mkdir(parents=True)
    shutil.copytree(ROOT / "apps" / "desktop", app_dir / "apps" / "desktop")
    shutil.copytree(ROOT / PET_ANIMATION_PACKAGE_REL, app_dir / PET_ANIMATION_PACKAGE_REL)
    shutil.copytree(ROOT / "packages" / "pet_package_schema", app_dir / "packages" / "pet_package_schema")
    required_files = list(V02_DESKTOP_REQUIRED_FILES)
    if launch_mode != "cloud-create":
        shutil.copytree(ROOT / "services", app_dir / "services")
        shutil.copytree(ROOT / "workers", app_dir / "workers")
        (app_dir / "tests" / "fixtures").mkdir(parents=True, exist_ok=True)
        shutil.copytree(ROOT / "tests" / "fixtures" / "public", app_dir / "tests" / "fixtures" / "public")
        (app_dir / "scripts").mkdir(parents=True, exist_ok=True)
        shutil.copy2(ROOT / "scripts" / "run_production_local.py", app_dir / "scripts" / "run_production_local.py")
        required_files.extend(LOCAL_CREATE_REQUIRED_FILES)
    for required in required_files:
        if not (app_dir / required).exists():
            raise FileNotFoundError(f"required desktop app file missing from package: {required}")
    main_entry = "apps/desktop/main.cjs"
    if launch_mode in {"create", "cloud-create"}:
        launcher = app_dir / "apps" / "desktop" / "packaged-create-main.cjs"
        launcher_lines = [
            "process.env.AI_PET_DESKTOP_CREATE = '1';",
            f"process.env.AI_PET_BUILD_RUNNER = process.env.AI_PET_BUILD_RUNNER || {json.dumps(build_runner)};",
        ]
        if launch_mode == "cloud-create":
            launcher_lines.extend(
                [
                    "process.env.AI_PET_DESKTOP_LAUNCH_MODE = 'cloud-create';",
                    "process.env.AI_PET_DISABLE_LOCAL_SERVICE = '1';",
                    f"process.env.SIXXIE_API_BASE_URL = process.env.SIXXIE_API_BASE_URL || {json.dumps(str(api_base_url))};",
                ]
            )
        if env_path is not None:
            launcher_lines.append(
                f"process.env.AI_PET_DESKTOP_ENV_PATH = process.env.AI_PET_DESKTOP_ENV_PATH || {json.dumps(str(env_path))};"
            )
        launcher_lines.append("require('./main.cjs');")
        launcher.write_text("\n".join(launcher_lines) + "\n", encoding="utf-8")
        main_entry = "apps/desktop/packaged-create-main.cjs"
    package_json = {
        "name": "ai-memorial-desktop-pet-packaged",
        "version": "0.1.0",
        "private": True,
        "type": "commonjs",
        "main": main_entry,
    }
    (app_dir / "package.json").write_text(json.dumps(package_json, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def _install_pet_package(app_path: Path, pet_package: Path) -> None:
    target_root = app_path / PET_PACKAGES_RESOURCE_REL
    target = target_root / PET_NAME
    if target.exists():
        shutil.rmtree(target)
    target_root.mkdir(parents=True, exist_ok=True)
    shutil.copytree(pet_package, target)


def _select_sign_identity(sign_identity: str) -> dict[str, str]:
    if sign_identity and sign_identity not in {"auto", "-"}:
        return {"kind": "developer_id", "identity": sign_identity, "display": sign_identity}
    if sign_identity == "-":
        return {"kind": "ad_hoc", "identity": "-", "display": "ad-hoc (-)"}

    identities = _run(["security", "find-identity", "-v", "-p", "codesigning"], check=False)
    for line in identities["stdout"].splitlines():
        match = re.search(r'"(Developer ID Application: [^"]+)"', line)
        if match:
            identity = match.group(1)
            return {"kind": "developer_id", "identity": identity, "display": identity}
    return {"kind": "ad_hoc", "identity": "-", "display": "ad-hoc (-)"}


def _codesign_app(*, app_path: Path, identity: str, entitlements: Path | None) -> dict[str, Any]:
    command = ["codesign", "--force", "--deep", "--sign", identity]
    if identity != "-":
        command.extend(["--options", "runtime", "--timestamp"])
    if entitlements is not None:
        command.extend(["--entitlements", str(entitlements)])
    command.append(str(app_path))
    signed = _run(command, check=False)
    verified = _run(["codesign", "--verify", "--deep", "--strict", "--verbose=2", str(app_path)], check=False)
    return {
        "ok": signed["returncode"] == 0 and verified["returncode"] == 0,
        "sign": signed,
        "verify": verified,
    }


def _electron_entitlements_plist(staging_root: Path) -> Path:
    entitlements_path = staging_root / "electron-hardened-runtime-entitlements.plist"
    entitlements = {
        "com.apple.security.cs.allow-jit": True,
        "com.apple.security.cs.allow-unsigned-executable-memory": True,
        "com.apple.security.cs.disable-library-validation": True,
    }
    entitlements_path.write_bytes(plistlib.dumps(entitlements, sort_keys=True))
    return entitlements_path


def _codesign_electron_helper_apps(*, app_path: Path, identity: str, entitlements: Path | None) -> dict[str, Any]:
    helper_apps = _electron_helper_apps(app_path)
    sign_results = []
    verify_results = []
    for helper_app in helper_apps:
        command = ["codesign", "--force", "--deep", "--sign", identity]
        if identity != "-":
            command.extend(["--options", "runtime", "--timestamp"])
        if entitlements is not None:
            command.extend(["--entitlements", str(entitlements)])
        command.append(str(helper_app))
        sign_results.append(_run(command, check=False))
        verify_results.append(_run(["codesign", "--verify", "--deep", "--strict", "--verbose=2", str(helper_app)], check=False))
    return {
        "ok": bool(helper_apps)
        and all(result["returncode"] == 0 for result in sign_results)
        and all(result["returncode"] == 0 for result in verify_results),
        "helper_app_count": len(helper_apps),
        "helper_apps": [str(helper_app.relative_to(app_path)) for helper_app in helper_apps],
        "sign": sign_results,
        "verify": verify_results,
    }


def _codesign_electron_nested_runtime(*, app_path: Path, identity: str) -> dict[str, Any]:
    libraries = _electron_nested_runtime_libraries(app_path)
    sign_results = []
    verify_results = []
    for library in libraries:
        sign_command = ["codesign", "--force", "--sign", identity]
        if identity != "-":
            sign_command.extend(["--options", "runtime", "--timestamp"])
        sign_command.append(str(library))
        sign_results.append(_run(sign_command, check=False))
        verify_results.append(_run(["codesign", "--verify", "--strict", "--verbose=2", str(library)], check=False))
    return {
        "ok": bool(libraries)
        and all(result["returncode"] == 0 for result in sign_results)
        and all(result["returncode"] == 0 for result in verify_results),
        "library_count": len(libraries),
        "libraries": [str(library.relative_to(app_path)) for library in libraries],
        "sign": sign_results,
        "verify": verify_results,
    }


def _check_electron_nested_runtime_team(*, app_path: Path, identity: str) -> dict[str, Any]:
    expected_team = _expected_team_identifier(identity)
    checks = []
    for library in _electron_nested_runtime_libraries(app_path):
        details = _codesign_details(library)
        team = _codesign_detail_value(details["stderr"], "TeamIdentifier")
        checks.append(
            {
                "path": str(library.relative_to(app_path)),
                "team_identifier": team,
                "expected_team_identifier": expected_team,
                "ok": team == expected_team if expected_team else team in {None, "not set"},
                "details": details,
            }
        )
    return {
        "ok": bool(checks) and all(check["ok"] for check in checks),
        "expected_team_identifier": expected_team,
        "checks": checks,
    }


def _electron_helper_apps(app_path: Path) -> list[Path]:
    frameworks_dir = app_path / "Contents" / "Frameworks"
    helper_apps = sorted(frameworks_dir.glob("Electron Helper*.app")) if frameworks_dir.exists() else []
    if not helper_apps:
        raise FileNotFoundError(f"Electron helper apps not found in: {frameworks_dir}")
    return helper_apps


def _electron_nested_runtime_libraries(app_path: Path) -> list[Path]:
    libraries_dir = (
        app_path
        / "Contents"
        / "Frameworks"
        / "Electron Framework.framework"
        / "Versions/A/Libraries"
    )
    libraries = sorted(libraries_dir.glob("*.dylib")) if libraries_dir.exists() else []
    if not any(library.name == "libffmpeg.dylib" for library in libraries):
        raise FileNotFoundError(f"libffmpeg.dylib not found in Electron nested runtime libraries: {libraries_dir}")
    return libraries


def _expected_team_identifier(identity: str) -> str | None:
    if identity == "-":
        return None
    match = re.search(r"\(([A-Z0-9]{10})\)\s*$", identity)
    return match.group(1) if match else None


def _codesign_details(path: Path) -> dict[str, Any]:
    return _run(["codesign", "-dv", "--verbose=4", str(path)], check=False)


def _codesign_detail_value(details: str, key: str) -> str | None:
    for line in details.splitlines():
        if line.startswith(f"{key}="):
            value = line.split("=", 1)[1].strip()
            return None if value == "not set" else value
    return None


def _strip_extended_attributes(app_path: Path) -> dict[str, Any]:
    return _run(["xattr", "-cr", str(app_path)], check=False)


def _create_dmg(*, app_path: Path, dmg_path: Path, release_id: str) -> dict[str, Any]:
    result = _run(
        [
            "hdiutil",
            "create",
            "-volname",
            f"{APP_NAME} {release_id}",
            "-srcfolder",
            str(app_path),
            "-ov",
            "-format",
            "UDZO",
            str(dmg_path),
        ],
        check=False,
    )
    verify = _run(["hdiutil", "verify", str(dmg_path)], check=False) if dmg_path.exists() else _empty_result()
    return {"ok": result["returncode"] == 0 and verify["returncode"] == 0, "create": result, "verify": verify}


def _codesign_dmg_if_possible(*, dmg_path: Path, identity: str) -> dict[str, Any]:
    if not dmg_path.exists():
        return {"ok": False, "skipped": True, "reason": "dmg_missing"}
    result = _run(["codesign", "--force", "--sign", identity, str(dmg_path)], check=False)
    verify = _run(["codesign", "--verify", "--verbose=2", str(dmg_path)], check=False)
    return {"ok": result["returncode"] == 0 and verify["returncode"] == 0, "sign": result, "verify": verify}


def _run(command: list[str], *, check: bool) -> dict[str, Any]:
    completed = subprocess.run(command, cwd=ROOT, text=True, capture_output=True, check=False)
    result = {
        "command": command,
        "returncode": completed.returncode,
        "stdout": completed.stdout.strip(),
        "stderr": completed.stderr.strip(),
    }
    if check and completed.returncode != 0:
        raise RuntimeError(json.dumps(result, ensure_ascii=False, indent=2))
    return result


def _empty_result() -> dict[str, Any]:
    return {"command": [], "returncode": 1, "stdout": "", "stderr": ""}


def _sha256_file(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def _api_base_url_host(api_base_url: str | None) -> str | None:
    value = str(api_base_url or "").strip()
    if not value:
        return None
    return (urlparse(value).hostname or "").lower() or None


def _require_existing_dir(path: Path, label: str) -> None:
    if not path.exists() or not path.is_dir():
        raise FileNotFoundError(f"{label} directory not found: {path}")


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