#!/usr/bin/env python3
"""Install TekPartner instructions only. Python 3.9+; no packages or credentials."""
import argparse
import hashlib
import io
import json
import os
from pathlib import Path
import re
import shutil
import stat
import sys
import tempfile
import urllib.request
import zipfile

BASE = "https://www.tekpartner.app/agent-skills/"
NAME = "tekpartner-v3"
FILES = {"SKILL.md", "references/agent-manual.md", "references/tek-local-gateway.md"}
LIMIT = 512 * 1024


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, *args, **kwargs):
        raise ValueError("Download redirected; use the published www.tekpartner.app installer")


def download(name, limit):
    # Only fixed manifest/validated archive filenames reach this function.
    with urllib.request.build_opener(NoRedirect).open(BASE + name, timeout=30) as response:
        data = response.read(limit + 1)
    if len(data) > limit:
        raise ValueError("Download exceeds the package size limit")
    return data


def unpack(manifest, payload):
    version = manifest.get("version", "")
    if (manifest.get("format") != 1 or manifest.get("name") != NAME
            or not isinstance(version, str) or not re.fullmatch(r"\d+\.\d+\.\d+", version)
            or manifest.get("archive") != f"{NAME}-{version}.zip"):
        raise ValueError("Unsupported skill manifest")
    if (len(payload) > LIMIT or manifest.get("sizeBytes") != len(payload)
            or manifest.get("sha256") != hashlib.sha256(payload).hexdigest()):
        raise ValueError("Archive size/checksum mismatch; nothing installed")
    if not isinstance(manifest.get("files"), dict) or set(manifest["files"]) != FILES:
        raise ValueError("Unexpected manifest files")
    result = {}
    with zipfile.ZipFile(io.BytesIO(payload)) as archive:
        entries = archive.infolist()
        expected = {f"{NAME}/{name}" for name in FILES}
        if len(entries) != len(FILES) or {entry.filename for entry in entries} != expected:
            raise ValueError("Unexpected, duplicate or unsafe archive paths")
        if sum(entry.file_size for entry in entries) > LIMIT:
            raise ValueError("Expanded archive exceeds limit")
        for entry in entries:
            if (entry.is_dir() or entry.flag_bits & 1
                    or stat.S_IFMT(entry.external_attr >> 16) not in (0, stat.S_IFREG)):
                raise ValueError("Archive must contain ordinary instruction files")
            name = entry.filename[len(NAME) + 1:]
            content = archive.read(entry)
            content.decode("utf-8")
            if hashlib.sha256(content).hexdigest() != manifest["files"][name]:
                raise ValueError("Instruction checksum mismatch")
            result[name] = content
    return result


def no_symlinks(path):
    for part in (path, *path.parents):
        if part.is_symlink():
            raise ValueError(f"Refusing symlink path: {part}")


def codex_directory():
    configured = os.environ.get("CODEX_HOME")
    if configured:
        return Path(configured).expanduser() / "skills"
    candidates = [Path.home() / ".agents/skills", Path.home() / ".codex/skills"]
    installed = [path for path in candidates if (path / NAME).exists()]
    if len(installed) > 1:
        raise ValueError("Multiple Codex copies found; choose --directory and keep one active skill copy")
    return installed[0] if installed else candidates[0]


def install(root, files, dry_run=False):
    root = Path(os.path.abspath(root.expanduser()))
    target = root / NAME
    no_symlinks(target)
    if target.exists() and not target.is_dir():
        raise ValueError(f"Skill target is not a directory: {target}")
    if dry_run:
        return f"Would install {len(files)} verified files to {target}; existing skill would be backed up"
    root.mkdir(parents=True, exist_ok=True)
    lock = root / f".{NAME}.install-lock"
    try:
        descriptor = os.open(lock, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
    except FileExistsError:
        raise ValueError(f"Install lock exists: {lock}. Check for another installer before recovering an interrupted install") from None
    os.close(descriptor)
    staging = None
    backup = None
    try:
        no_symlinks(target)
        if target.exists():
            existing = list(target.rglob("*"))
            if (not any(path.is_symlink() for path in existing)
                    and {path.relative_to(target).as_posix() for path in existing if path.is_file()} == set(files)
                    and all((target / name).read_bytes() == data for name, data in files.items())):
                return f"Already current: {target} (no backup needed)"
        staging = Path(tempfile.mkdtemp(prefix=f".{NAME}-", dir=root))
        for name, data in files.items():
            path = staging / name
            path.parent.mkdir(parents=True, exist_ok=True)
            path.write_bytes(data)
        if target.exists():
            backups = root.parent / "skill-backups"
            no_symlinks(backups)
            backups.mkdir(mode=0o700, exist_ok=True)
            container = Path(tempfile.mkdtemp(prefix=f"{NAME}-", dir=backups))
            backup = container / NAME
            target.rename(backup)
        try:
            staging.rename(target)
            staging = None
        except BaseException:
            if backup and not target.exists():
                backup.rename(target)
            raise
        return f"Installed {target}" + (f"\nPrevious skill preserved at {backup}" if backup else "")
    finally:
        if staging and staging.exists():
            shutil.rmtree(staging)
        lock.unlink()


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--client", choices=("codex", "claude", "both"), required=True)
    parser.add_argument("--directory", type=Path, help="Custom skills parent directory (one client only)")
    parser.add_argument("--dry-run", action="store_true", help="Validate downloads and show destination without installing")
    args = parser.parse_args()
    if args.directory and args.client == "both":
        parser.error("--directory requires a single client")
    manifest = json.loads(download("manifest.json", 16 * 1024))
    # Validate before using the filename in a request.
    if (not isinstance(manifest, dict) or not isinstance(manifest.get("version"), str)
            or not re.fullmatch(r"\d+\.\d+\.\d+", manifest["version"])
            or manifest.get("archive") != f"{NAME}-{manifest['version']}.zip"):
        raise ValueError("Invalid release manifest")
    files = unpack(manifest, download(manifest["archive"], LIMIT))
    clients = ("codex", "claude") if args.client == "both" else (args.client,)
    for client in clients:
        directory = args.directory or (codex_directory() if client == "codex" else Path.home() / ".claude/skills")
        print(install(directory, files, args.dry_run))
    print(f"Release {manifest['version']} verified. Credentials/configuration unchanged. Restart the client to discover the skill.")
    print("Next: open your project and ask the tekpartner-v3 skill to set up your agent connection and session-reporting preference with you. Never paste a key into chat.")


if __name__ == "__main__":
    try:
        main()
    except (OSError, ValueError, KeyError, zipfile.BadZipFile) as error:
        print(f"Installation failed: {error}", file=sys.stderr)
        sys.exit(1)
