#!/usr/bin/env python3
"""Standalone self-update script for mesh-optimizer.

Works even if the mesh package is partially broken — uses only stdlib
for core operations, importing mesh modules only when available.

Usage:
    python -m mesh.scripts.self_update --check
    python -m mesh.scripts.self_update --apply <package_path>
    python -m mesh.scripts.self_update --apply-url <url>
    python -m mesh.scripts.self_update --rollback
    python -m mesh.scripts.self_update --version
"""
from __future__ import annotations

import argparse
import hashlib
import json
import os
import shutil
import sys
import tarfile
import tempfile
import time
from datetime import datetime
from pathlib import Path


def _project_root() -> Path:
    """Find the project root (parent of mesh/)."""
    # Try relative to this script
    script_dir = Path(__file__).resolve().parent
    root = script_dir.parents[1]
    if (root / "mesh").is_dir():
        return root
    # Fallback: cwd
    if (Path.cwd() / "mesh").is_dir():
        return Path.cwd()
    return root


def _get_version(root: Path) -> str:
    """Read the current version from pyproject.toml."""
    pyproject = root / "pyproject.toml"
    if pyproject.exists():
        for line in pyproject.read_text().splitlines():
            line = line.strip()
            if line.startswith("version"):
                # version = "0.1.0"
                return line.split("=", 1)[1].strip().strip('"').strip("'")
    return "unknown"


def _compute_sha256(path: Path) -> str:
    sha = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            sha.update(chunk)
    return sha.hexdigest()


def _load_history(root: Path) -> dict:
    hp = root / "data" / "update_history.json"
    if hp.exists():
        return json.loads(hp.read_text())
    return {"current_version": _get_version(root), "entries": []}


def _save_history(root: Path, history: dict) -> None:
    hp = root / "data" / "update_history.json"
    hp.parent.mkdir(parents=True, exist_ok=True)
    hp.write_text(json.dumps(history, indent=2, default=str))


def cmd_version(root: Path) -> None:
    version = _get_version(root)
    print(f"mesh-optimizer v{version}")
    print(f"Install dir: {root}")


def cmd_check(root: Path, server_url: str, channel: str) -> None:
    if not server_url:
        print("No update server configured. Use --server-url or set update.update_server_url in config.")
        return

    import urllib.request
    import urllib.error

    url = f"{server_url.rstrip('/')}/updates/latest?channel={channel}"
    try:
        req = urllib.request.Request(url, headers={"Accept": "application/json"})
        with urllib.request.urlopen(req, timeout=10) as resp:
            data = json.loads(resp.read())
    except urllib.error.URLError as e:
        print(f"Failed to contact update server: {e}")
        return
    except Exception as e:
        print(f"Error: {e}")
        return

    current = _get_version(root)
    remote = data.get("version", "unknown")

    print(f"Current version: {current}")
    print(f"Latest version:  {remote} ({channel})")

    if data.get("release_notes"):
        print(f"Release notes:   {data['release_notes']}")
    if data.get("size_bytes"):
        print(f"Package size:    {data['size_bytes'] / 1048576:.1f} MB")

    try:
        cur = tuple(int(x) for x in current.split("."))
        rem = tuple(int(x) for x in remote.split("."))
        if rem > cur:
            print("\nUpdate available! Run with --apply-url to update.")
        else:
            print("\nAlready up to date.")
    except (ValueError, AttributeError):
        print("\nCould not compare versions.")


def cmd_apply(root: Path, package_path: str, sha256: str = "") -> None:
    pkg = Path(package_path)
    if not pkg.exists():
        print(f"Package not found: {pkg}")
        sys.exit(1)

    version = _get_version(root)
    print(f"Current version: {version}")
    print(f"Applying package: {pkg}")

    # Verify SHA-256
    if sha256:
        actual = _compute_sha256(pkg)
        if actual != sha256:
            print(f"SHA-256 MISMATCH!")
            print(f"  Expected: {sha256}")
            print(f"  Actual:   {actual}")
            sys.exit(1)
        print("SHA-256 verified.")

    # Create backup
    backup_dir = root / "data" / "backups"
    backup_dir.mkdir(parents=True, exist_ok=True)
    ts = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
    backup_path = backup_dir / f"mesh-{version}-{ts}"

    mesh_src = root / "mesh"
    if mesh_src.is_dir():
        print(f"Backing up to {backup_path}")
        shutil.copytree(mesh_src, backup_path / "mesh", symlinks=True)
        pyproject = root / "pyproject.toml"
        if pyproject.exists():
            shutil.copy2(pyproject, backup_path / "pyproject.toml")
    else:
        print("WARNING: No mesh/ directory found to back up")

    # Extract
    print("Extracting update...")
    try:
        with tarfile.open(pkg, "r:*") as tf:
            # Security: check for path traversal
            for member in tf.getmembers():
                mp = (root / member.name).resolve()
                try:
                    mp.relative_to(root.resolve())
                except ValueError:
                    print(f"SECURITY: Path traversal detected: {member.name}")
                    sys.exit(1)
            tf.extractall(root, filter="data")
    except Exception as e:
        print(f"Extraction failed: {e}")
        print("Restoring backup...")
        _restore_backup(root, backup_path)
        sys.exit(1)

    # Update history
    new_version = _get_version(root)
    history = _load_history(root)
    history["current_version"] = new_version
    history["entries"].append({
        "version": new_version,
        "status": "complete",
        "applied_at": datetime.utcnow().isoformat(),
        "backup_path": str(backup_path),
    })
    _save_history(root, history)

    print(f"Update complete: {version} -> {new_version}")
    print("Restart the service to use the new version.")


def cmd_apply_url(root: Path, url: str, sha256: str = "") -> None:
    import urllib.request

    print(f"Downloading: {url}")
    tmp = Path(tempfile.mkdtemp(prefix="mesh-update-"))
    dest = tmp / "mesh-update.tar.gz"

    try:
        urllib.request.urlretrieve(url, str(dest))
    except Exception as e:
        print(f"Download failed: {e}")
        sys.exit(1)

    print(f"Downloaded {dest.stat().st_size / 1048576:.1f} MB")
    cmd_apply(root, str(dest), sha256=sha256)


def cmd_rollback(root: Path) -> None:
    history = _load_history(root)
    entries = history.get("entries", [])

    # Find the last entry with a backup
    for entry in reversed(entries):
        bp = entry.get("backup_path")
        if bp and Path(bp).exists():
            print(f"Rolling back from backup: {bp}")
            _restore_backup(root, Path(bp))

            entry["status"] = "rolled_back"
            entry["rolled_back_at"] = datetime.utcnow().isoformat()

            # Restore version from backup dir name
            parts = Path(bp).name.split("-")
            if len(parts) >= 2:
                history["current_version"] = parts[1]

            _save_history(root, history)
            print(f"Rollback complete. Version: {history['current_version']}")
            return

    print("No backup available for rollback.")
    sys.exit(1)


def _restore_backup(root: Path, backup_path: Path) -> None:
    mesh_backup = backup_path / "mesh"
    mesh_dst = root / "mesh"

    if mesh_backup.is_dir():
        if mesh_dst.exists():
            shutil.rmtree(mesh_dst)
        shutil.copytree(mesh_backup, mesh_dst, symlinks=True)

    pyproject_backup = backup_path / "pyproject.toml"
    if pyproject_backup.exists():
        shutil.copy2(pyproject_backup, root / "pyproject.toml")


def main():
    parser = argparse.ArgumentParser(
        description="Mesh-optimizer self-update utility",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("--check", action="store_true", help="Check for updates")
    parser.add_argument("--apply", metavar="PATH", help="Apply update from local file")
    parser.add_argument("--apply-url", metavar="URL", help="Download and apply update")
    parser.add_argument("--rollback", action="store_true", help="Rollback to previous version")
    parser.add_argument("--version", action="store_true", help="Show current version")
    parser.add_argument("--sha256", default="", help="Expected SHA-256 of package")
    parser.add_argument("--server-url", default="", help="Update server URL")
    parser.add_argument("--channel", default="stable", help="Update channel")
    parser.add_argument("--install-dir", default="", help="Override install directory")

    args = parser.parse_args()
    root = Path(args.install_dir) if args.install_dir else _project_root()

    if args.version:
        cmd_version(root)
    elif args.check:
        cmd_check(root, args.server_url, args.channel)
    elif args.apply:
        cmd_apply(root, args.apply, sha256=args.sha256)
    elif args.apply_url:
        cmd_apply_url(root, args.apply_url, sha256=args.sha256)
    elif args.rollback:
        cmd_rollback(root)
    else:
        parser.print_help()


if __name__ == "__main__":
    main()
