#!/usr/bin/env python3
"""Syncropy Setup — One-command encrypted sync between MASCOM universes.

For owners (John & Ron) who already share an mhsync .key file:
    curl -sL syncropy.com/install | python3 - --key-file /path/to/mhsync.key

For new users:
    curl -sL syncropy.com/install | python3
    curl -sL syncropy.com/install | python3 - --mesh-key HEX_KEY

This script:
  1. Detects your machine identity
  2. Uses your shared .key to authenticate with the Syncropy relay
  3. Registers you with the Syncropy hub
  4. Auto-links your universe when your partner also registers
  5. Generates authority.json for conflict resolution
  6. Verifies relay connectivity end-to-end
  7. Installs a Claude Code hook so sync starts automatically
"""

import os
import sys
import json
import hashlib
import hmac as _hmac
import secrets
import argparse
import subprocess
import platform
import urllib.request
import urllib.error
from pathlib import Path
from datetime import datetime

SYNCROPY_VERSION = "2.0.0"
RELAY_URL = "wss://mhsync-relay.johnmobley99.workers.dev"
API_URL = "https://syncropy-com-api.johnmobley99.workers.dev"

# Known owner machines (auto-detected from hostname)
KNOWN_OWNERS = {
    "JOHNS-MACBOOK-PRO": {"name": "JOHN_MAC", "universe": "MASCOM", "role": "Chief Architect"},
    "JOHNS_MACBOOK_PRO": {"name": "JOHN_MAC", "universe": "MASCOM", "role": "Chief Architect"},
    "JOHN_MAC":          {"name": "JOHN_MAC", "universe": "MASCOM", "role": "Chief Architect"},
    "DESKTOP-RON":       {"name": "RON_WINDOWS", "universe": "HASCOM", "role": "Lead Developer"},
    "RON_WINDOWS":       {"name": "RON_WINDOWS", "universe": "HASCOM", "role": "Lead Developer"},
    "RON-PC":            {"name": "RON_WINDOWS", "universe": "HASCOM", "role": "Lead Developer"},
}

# Standard sync roots for MASCOM <-> HASCOM
OWNER_SYNC_ROOTS = [
    {"root_id": "mhsync_rules", "authority": "JOHN_MAC", "description": "MHSync rules"},
    {"root_id": "phase2_tickets", "authority": "AI_MERGE", "fallback": "JOHN_MAC", "description": "Phase 2 tickets"},
    {"root_id": "phase2_rules", "authority": "AI_MERGE", "fallback": "JOHN_MAC", "description": "Phase 2 rules"},
    {"root_id": "quanticfork", "authority": "RON_WINDOWS", "description": "QuanticFork (SubX)"},
    {"root_id": "mhsync_code", "authority": "JOHN_MAC", "description": "Sync engine code"},
    {"root_id": "mhscom", "authority": "AI_MERGE", "fallback": "JOHN_MAC", "description": "MHSCOM artifacts"},
]

# ============================================================
# Colors
# ============================================================
class C:
    CYAN = "\033[96m"
    GREEN = "\033[92m"
    GOLD = "\033[93m"
    RED = "\033[91m"
    DIM = "\033[90m"
    BOLD = "\033[1m"
    RESET = "\033[0m"

def banner():
    print(f"""
{C.CYAN}{C.BOLD}============================================================
  SYNCROPY v{SYNCROPY_VERSION}
  Encrypted Sync | Auto-Link | Zero Trust
============================================================{C.RESET}
{C.DIM}AES-256-GCM end-to-end encryption via shared .key file{C.RESET}
""")

def step(msg):
    print(f"  {C.CYAN}>{C.RESET} {msg}", end="", flush=True)

def done(detail="done"):
    print(f"  {C.GREEN}{detail}{C.RESET}")

def fail(detail="failed"):
    print(f"  {C.RED}{detail}{C.RESET}")

def warn(detail):
    print(f"  {C.GOLD}{detail}{C.RESET}")

# ============================================================
# Platform detection
# ============================================================
def get_platform():
    s = platform.system().lower()
    if s == "darwin": return "mac"
    elif s == "windows": return "windows"
    else: return "linux"

def get_home():
    return Path.home()

def get_syncropy_dir():
    home = get_home()
    plat = get_platform()
    if plat == "mac":
        return home / ".syncropy"
    elif plat == "windows":
        return home / "AppData" / "Local" / "Syncropy"
    else:
        return home / ".syncropy"

# ============================================================
# Identity detection
# ============================================================
def detect_identity(override_name=None):
    """Detect machine identity. Returns (machine_name, universe, role, is_owner)."""
    hostname = (override_name or platform.node()).replace(".", "_").replace("-", "_").upper()

    # Check known owners
    for pattern, info in KNOWN_OWNERS.items():
        if pattern in hostname or hostname in pattern:
            return info["name"], info["universe"], info["role"], True

    # Check environment variables
    env_name = os.environ.get("MASCOM_MACHINE", os.environ.get("SYNCROPY_MACHINE", ""))
    if env_name.upper() in KNOWN_OWNERS:
        info = KNOWN_OWNERS[env_name.upper()]
        return info["name"], info["universe"], info["role"], True

    # Unknown machine
    clean_name = hostname or f"NODE_{secrets.token_hex(4).upper()}"
    return clean_name, None, "member", False

# ============================================================
# Crypto helpers
# ============================================================
def derive_token(key: bytes, purpose: str) -> str:
    return _hmac.new(key, purpose.encode(), hashlib.sha256).hexdigest()

def derive_node_id(key: bytes, machine_name: str) -> str:
    return derive_token(key, f"mhsync-node-id-v1:{machine_name}")[:16]

def derive_relay_token(key: bytes) -> str:
    return derive_token(key, "mhsync-relay-v1")

# ============================================================
# Key file handling
# ============================================================
def find_key_file(explicit_path=None):
    """Find the shared .key file. Searches common locations."""
    candidates = []

    if explicit_path:
        candidates.append(Path(explicit_path))

    home = get_home()
    candidates.extend([
        # MHSync standard locations
        home / "mascom" / "MASCOM" / "MHS" / "mhsync" / "mhsync.key",
        home / "MASCOM" / "MHS" / "mhsync" / "mhsync.key",
        home / "hascom" / "HASCOM" / "MHS" / "mhsync" / "mhsync.key",
        # Syncropy locations
        home / ".syncropy" / "syncropy.key",
        home / "AppData" / "Local" / "Syncropy" / "syncropy.key",
        # Current directory
        Path("mhsync.key"),
        Path(".key"),
    ])

    for p in candidates:
        try:
            p = p.expanduser().resolve()
            if p.exists() and p.stat().st_size > 0:
                return p
        except (OSError, ValueError):
            continue

    return None

def load_key(path: Path) -> bytes:
    """Load hex-encoded 256-bit key."""
    raw = path.read_text().strip()
    if len(raw) == 64:
        return bytes.fromhex(raw)
    raise ValueError(f"Key file must contain 64 hex characters, got {len(raw)}")

# ============================================================
# API calls
# ============================================================
def api_call(endpoint, data=None, headers_extra=None, method="POST"):
    """Make an API call to the Syncropy worker."""
    url = f"{API_URL}{endpoint}"
    hdrs = {"Content-Type": "application/json"}
    if headers_extra:
        hdrs.update(headers_extra)

    body = json.dumps(data).encode() if data else None
    req = urllib.request.Request(url, data=body, headers=hdrs, method=method)

    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
            return json.loads(resp.read()), resp.status
    except urllib.error.HTTPError as e:
        try:
            body = json.loads(e.read())
        except Exception:
            body = {"error": str(e)}
        return body, e.code
    except Exception as e:
        return {"error": str(e)}, 0

def register_owner(key: bytes, machine_name: str, universe: str):
    """Register as a universe owner with the Syncropy hub."""
    key_hash = hashlib.sha256(key).hexdigest()
    return api_call("/api/mhscom/register-owner", {
        "machine_name": machine_name,
        "universe": universe,
    }, headers_extra={"X-Owner-Key": key.hex()})

def check_link_status(key: bytes):
    """Check if both universes are linked."""
    return api_call("/api/auth/link-status", method="GET",
                    headers_extra={"X-Owner-Key": key.hex()})

def auto_link(key: bytes, machine_name: str, universe: str):
    """Trigger auto-linking between universes."""
    return api_call("/api/auth/auto-link", {
        "machine_name": machine_name,
        "universe": universe,
    }, headers_extra={"X-Owner-Key": key.hex()})

def get_authority(key: bytes):
    """Get auto-generated authority.json."""
    return api_call("/api/auth/authority", method="GET",
                    headers_extra={"X-Owner-Key": key.hex()})

def verify_relay(key: bytes, machine_name: str):
    """Verify relay connectivity by checking the health endpoint."""
    token = derive_relay_token(key)
    node = derive_node_id(key, machine_name)
    return api_call("/api/health", method="GET")

# ============================================================
# Configuration
# ============================================================
def create_config(syncropy_dir: Path, key_path: Path, machine_name: str,
                  universe: str, partner_name: str, sync_roots: list):
    config = {
        "version": SYNCROPY_VERSION,
        "machine_name": machine_name,
        "universe": universe,
        "sync_port": 7777,
        "partner": {
            "machine_name": partner_name,
        },
        "psk_file": str(key_path),
        "relay": {"url": RELAY_URL},
        "api_url": API_URL,
        "sync_settings": {
            "sync_interval": 2.0,
            "reconnect_delay": 5.0,
        },
        "sync_roots": [r["root_id"] for r in sync_roots],
        "authority": {r["root_id"]: {
            "default_authority": r["authority"],
            **({"fallback": r.get("fallback")} if r.get("fallback") else {}),
        } for r in sync_roots},
        "ignore_patterns": [
            "__pycache__", ".git", ".DS_Store", "Thumbs.db",
            "desktop.ini", "node_modules", "*.pyc", "venv",
            ".syncropy", ".pytest_cache"
        ],
        "installed_at": datetime.now().isoformat(),
    }
    config_path = syncropy_dir / "config.json"
    config_path.write_text(json.dumps(config, indent=2))
    return config_path

# ============================================================
# Claude Code integration
# ============================================================
def install_claude_code_hook(syncropy_dir: Path, key_path: Path, machine_name: str):
    """Install a Claude Code settings snippet for syncropy."""
    claude_dir = get_home() / ".claude"
    if not claude_dir.exists():
        return False

    # Write syncropy config that Claude Code can reference
    sync_config = {
        "syncropy": {
            "enabled": True,
            "version": SYNCROPY_VERSION,
            "key_file": str(key_path),
            "machine_name": machine_name,
            "relay": RELAY_URL,
            "api": API_URL,
            "config": str(syncropy_dir / "config.json"),
        }
    }
    sync_path = claude_dir / "syncropy.json"
    sync_path.write_text(json.dumps(sync_config, indent=2))
    return True

# ============================================================
# Dependency check
# ============================================================
def check_deps():
    missing = []
    for mod in ["websockets", "cryptography", "watchdog"]:
        try:
            __import__(mod)
        except ImportError:
            missing.append(mod)
    return missing

def install_deps(missing):
    step(f"Installing dependencies ({', '.join(missing)})...")
    try:
        subprocess.check_call(
            [sys.executable, "-m", "pip", "install", "--quiet"] + missing,
            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
        )
        done()
        return True
    except subprocess.CalledProcessError:
        fail()
        print(f"    {C.DIM}Run: pip install {' '.join(missing)}{C.RESET}")
        return False

# ============================================================
# Main setup
# ============================================================
def setup(args):
    banner()

    plat = get_platform()
    home = get_home()
    syncropy_dir = get_syncropy_dir()

    # === Step 1: Detect identity ===
    machine_name, universe, role, is_owner = detect_identity(args.machine_name)
    partner_name = "RON_WINDOWS" if machine_name == "JOHN_MAC" else "JOHN_MAC"

    print(f"  Platform:  {C.BOLD}{plat}{C.RESET}")
    print(f"  Machine:   {C.BOLD}{machine_name}{C.RESET}")
    if is_owner:
        print(f"  Universe:  {C.GOLD}{universe}{C.RESET}")
        print(f"  Role:      {C.BOLD}{role}{C.RESET}")
        print(f"  Partner:   {C.CYAN}{partner_name}{C.RESET}")
    print(f"  Directory: {C.DIM}{syncropy_dir}{C.RESET}")
    print()

    # === Step 2: Find shared .key file ===
    step("Locating shared .key file...")
    key_path = find_key_file(args.key_file)

    if key_path:
        done(str(key_path))
        key = load_key(key_path)
    elif args.mesh_key:
        key = bytes.fromhex(args.mesh_key) if len(args.mesh_key) == 64 else hashlib.sha256(args.mesh_key.encode()).digest()
        key_path = syncropy_dir / "syncropy.key"
        syncropy_dir.mkdir(parents=True, exist_ok=True)
        key_path.write_text(key.hex())
        done(f"using mesh key -> {key_path}")
    else:
        fail("not found")
        print(f"""
  {C.RED}No shared .key file found.{C.RESET}

  {C.BOLD}If you already have an mhsync.key file:{C.RESET}
    python3 <(curl -sL syncropy.com/install) --key-file /path/to/mhsync.key

  {C.BOLD}If you're setting up fresh:{C.RESET}
    python3 <(curl -sL syncropy.com/install) --mesh-key YOUR_SHARED_KEY

  {C.DIM}The .key file is a 64-character hex string (256-bit AES key).
  Both partners must use the SAME key file.{C.RESET}
""")
        sys.exit(1)

    # === Step 3: Create directory + copy key ===
    step("Setting up Syncropy directory...")
    syncropy_dir.mkdir(parents=True, exist_ok=True)
    syncropy_key = syncropy_dir / "syncropy.key"
    if not syncropy_key.exists() or syncropy_key.read_text().strip() != key.hex():
        syncropy_key.write_text(key.hex())
    try:
        os.chmod(syncropy_key, 0o600)
    except (OSError, NotImplementedError):
        pass
    done()

    # === Step 4: Check dependencies ===
    step("Checking dependencies...")
    missing = check_deps()
    if missing:
        done(f"{len(missing)} missing")
        if not install_deps(missing):
            warn("Dependencies missing — sync will work but some features limited")
    else:
        done("all present")

    # === Step 5: Derive identifiers ===
    node_id = derive_node_id(key, machine_name)
    relay_token = derive_relay_token(key)[:16]

    step("Deriving node identity...")
    done(f"node={node_id}, relay={relay_token}...")

    # === Step 6: Register with Syncropy hub ===
    if is_owner and universe:
        step(f"Registering {universe} owner with Syncropy hub...")
        result, status = register_owner(key, machine_name, universe)
        if status in (200, 201, 409):
            done(result.get("message", "registered"))
        else:
            warn(f"hub offline ({status}) — will auto-link when available")

        # === Step 7: Auto-link ===
        step("Attempting auto-link with partner universe...")
        result, status = auto_link(key, machine_name, universe)
        if status == 200 and result.get("linked"):
            done(f"LINKED with {partner_name}!")
        elif status == 200:
            done(f"registered — waiting for {partner_name} to also register")
        else:
            warn(f"auto-link pending ({result.get('error', 'partner not registered yet')})")

        # === Step 8: Check link + authority ===
        step("Checking link status...")
        result, status = check_link_status(key)
        if status == 200 and result.get("linked"):
            done("both universes linked!")
            step("Fetching authority config...")
            auth_result, auth_status = get_authority(key)
            if auth_status == 200 and "authority" in auth_result:
                auth_path = syncropy_dir / "authority.json"
                auth_path.write_text(json.dumps(auth_result["authority"], indent=2))
                done(f"saved to {auth_path}")
            else:
                warn("authority not yet generated")
        else:
            warn(f"partner hasn't registered yet — they should run this same script")
    else:
        step("Registering as mesh node...")
        result, status = api_call("/api/mesh/join", {
            "node_id": node_id,
            "machine_name": machine_name,
            "platform": plat,
            "capabilities": ["sync"],
        }, headers_extra={"X-Mesh-Key": key.hex()[:32]})
        if status == 200:
            done(f"{result.get('mesh_nodes', '?')} nodes in mesh")
        else:
            done("offline (will sync when relay available)")

    # === Step 9: Verify relay connectivity ===
    step("Verifying relay connectivity...")
    result, status = verify_relay(key, machine_name)
    if status == 200:
        done(f"relay online (v{result.get('version', '?')})")
    else:
        warn("relay not reachable — sync will retry automatically")

    # === Step 10: Write config ===
    step("Writing configuration...")
    sync_roots = OWNER_SYNC_ROOTS if is_owner else [
        {"root_id": "shared", "authority": "timestamp", "description": "Shared workspace"}
    ]
    config_path = create_config(syncropy_dir, syncropy_key, machine_name,
                                universe or "MESH", partner_name, sync_roots)
    done()

    # === Step 11: Claude Code integration ===
    step("Installing Claude Code integration...")
    if install_claude_code_hook(syncropy_dir, syncropy_key, machine_name):
        done("~/.claude/syncropy.json")
    else:
        done("skipped (no ~/.claude directory)")

    # === Summary ===
    print(f"""
{C.GREEN}{C.BOLD}{'='*60}
  Syncropy setup complete!
{'='*60}{C.RESET}

  {C.BOLD}Machine:{C.RESET}     {machine_name}
  {C.BOLD}Node ID:{C.RESET}     {node_id}
  {C.BOLD}Universe:{C.RESET}    {universe or 'MESH'}
  {C.BOLD}Key:{C.RESET}         {syncropy_key}
  {C.BOLD}Config:{C.RESET}      {config_path}
  {C.BOLD}Relay:{C.RESET}       {RELAY_URL}
""")

    if is_owner:
        print(f"""  {C.GOLD}Owner sync active.{C.RESET} Sync roots:""")
        for r in sync_roots:
            auth = r["authority"]
            fb = f" (fallback: {r['fallback']})" if r.get("fallback") else ""
            print(f"    {C.CYAN}{r['root_id']}{C.RESET} — authority: {auth}{fb}")

        print(f"""
  {C.BOLD}Your partner should run:{C.RESET}
  {C.CYAN}curl -sL syncropy.com/install | python3 - --key-file /path/to/mhsync.key{C.RESET}

  Once both registered, the sync auto-links.
  Authority.json is generated server-side — no file sharing needed.
""")
    else:
        print(f"""  {C.CYAN}To start syncing:{C.RESET}
    Run your mhsync daemon as usual — Syncropy manages the relay auth.

  {C.DIM}Share this key with teammates:{C.RESET}
  {C.BOLD}{key.hex()}{C.RESET}
""")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Syncropy Setup")
    parser.add_argument("--key-file", "-k", help="Path to existing shared .key file (mhsync.key)")
    parser.add_argument("--mesh-key", help="Hex key to join a compute network")
    parser.add_argument("--machine-name", help="Override machine name (e.g. JOHN_MAC, RON_WINDOWS)")
    parser.add_argument("--no-deps", action="store_true", help="Skip dependency installation")
    args = parser.parse_args()
    setup(args)
