"""
BizNode Delta Updater
=====================
Checks for updates and downloads only changed files — no need to
re-download the full zip package.

How it works:
  1. Reads local VERSION and computes checksums of all local files
  2. Sends checksums to server API to get a diff
  3. Downloads only new/changed files
  4. Removes files deleted from the package
  5. Updates local VERSION
  6. Optionally restarts services

Usage:
    python scripts/update_biznode.py                # Check + apply updates
    python scripts/update_biznode.py --check        # Check only (don't apply)
    python scripts/update_biznode.py --force         # Update even if same version
    python scripts/update_biznode.py --restart       # Restart services after update

Protected files (NEVER overwritten):
    .env, .env.*, identity/*.pem, memory/biznode.db*, backups/, logs/
"""

import argparse
import base64
import hashlib
import json
import os
import sys
import time
from pathlib import Path

try:
    import requests
except ImportError:
    print("ERROR: 'requests' module required. Run: pip install requests")
    sys.exit(1)

# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
PROJECT_ROOT = Path(__file__).resolve().parent.parent
VERSION_FILE = PROJECT_ROOT / "VERSION"
UPDATE_API = "https://biznode.1bz.biz/api/version.php"

# Files/dirs that must NEVER be overwritten (user data, secrets, runtime)
PROTECTED_PATTERNS = [
    ".env",
    ".env.",
    ".trial",
    ".registration",
    ".license_key",
    "identity/",
    "memory/biznode.db",
    "memory/qdrant/",
    "memory/ollama/",
    "backups/",
    "logs/",
    "bot.log",
    "dashboard.log",
    "backup/credentials.json",
    "backup/token.json",
    "backup/last_backup.json",
    "config/youtube_client_secret.json",
    "config/youtube_token.json",
]


def _is_protected(path: str) -> bool:
    """Check if a file path is protected from updates."""
    path_unix = path.replace("\\", "/")
    for pattern in PROTECTED_PATTERNS:
        if pattern.endswith("/"):
            if path_unix.startswith(pattern):
                return True
        elif path_unix == pattern or path_unix.startswith(pattern):
            return True
    return False


def get_local_version() -> str:
    """Read local VERSION file."""
    if VERSION_FILE.exists():
        return VERSION_FILE.read_text().strip()
    return "0.0.0"


def compute_local_checksums() -> dict:
    """Compute MD5 checksums of all local files (matching package structure)."""
    checksums = {}
    skip_dirs = {
        ".git", "__pycache__", "node_modules", "dist",
        "backups", "logs", "output", "snapshots", "jobfiles",
        "basic", "pro",  # Package dirs — not part of running instance
    }

    for root, dirs, files in os.walk(str(PROJECT_ROOT)):
        dirs[:] = [d for d in dirs if d not in skip_dirs and not d.startswith(".")]

        for filename in files:
            filepath = os.path.join(root, filename)
            rel_path = os.path.relpath(filepath, str(PROJECT_ROOT))
            rel_unix = rel_path.replace("\\", "/")

            # Skip binary/runtime files
            if any(rel_unix.startswith(p) for p in [
                "memory/qdrant/", "memory/ollama/", "backups/", "logs/",
            ]):
                continue
            if filename.endswith((".pyc", ".pyo", ".db", ".db-journal", ".db-wal")):
                continue
            if filename.startswith(".") and filename != ".gitkeep":
                continue

            try:
                with open(filepath, "rb") as f:
                    md5 = hashlib.md5(f.read()).hexdigest()
                checksums[rel_unix] = md5
            except (PermissionError, OSError):
                continue

    return checksums


def check_update(local_version: str, local_checksums: dict) -> dict:
    """Check server for available updates."""
    # First check version
    print(f"  Checking {UPDATE_API}...")
    r = requests.get(UPDATE_API, timeout=15)
    r.raise_for_status()
    info = r.json()

    server_version = info.get("version", "0.0.0")
    print(f"  Local version  : {local_version}")
    print(f"  Server version : {server_version}")

    if not info.get("ok"):
        print("  Server returned error:", info.get("error", "unknown"))
        return {"update_needed": False}

    # Get diff by posting our checksums
    print(f"  Comparing {len(local_checksums)} local files against server...")
    r = requests.post(
        f"{UPDATE_API}?diff={local_version}",
        json={"files": local_checksums},
        timeout=30,
    )
    r.raise_for_status()
    diff = r.json()

    return diff


def apply_update(diff: dict) -> int:
    """Download and apply changed files."""
    to_update = diff.get("to_update", [])
    to_delete = diff.get("to_delete", [])
    server_version = diff.get("server_version", "0.0.0")

    if not to_update and not to_delete:
        print("\n  No files to update.")
        return 0

    total_size = diff.get("total_download_size", 0)
    print(f"\n  Files to update : {len(to_update)}")
    print(f"  Files to remove : {len(to_delete)}")
    print(f"  Download size   : {total_size / 1024:.1f} KB")
    print()

    updated = 0
    skipped = 0
    errors = 0

    # Download changed files
    for i, file_info in enumerate(to_update, 1):
        path = file_info["path"]

        if _is_protected(path):
            print(f"  [{i}/{len(to_update)}] PROTECTED (skip): {path}")
            skipped += 1
            continue

        print(f"  [{i}/{len(to_update)}] Downloading: {path} ({file_info['size']} bytes)...", end=" ")

        try:
            r = requests.get(
                f"{UPDATE_API}?file={path}",
                timeout=30,
            )
            r.raise_for_status()
            data = r.json()

            if not data.get("ok"):
                print(f"FAILED ({data.get('error', '?')})")
                errors += 1
                continue

            # Decode and verify
            content = base64.b64decode(data["content_base64"])
            actual_md5 = hashlib.md5(content).hexdigest()

            if actual_md5 != file_info["md5"]:
                print(f"CHECKSUM MISMATCH (expected {file_info['md5']}, got {actual_md5})")
                errors += 1
                continue

            # Write file
            dest = PROJECT_ROOT / path
            dest.parent.mkdir(parents=True, exist_ok=True)
            with open(str(dest), "wb") as f:
                f.write(content)

            print("OK")
            updated += 1

        except Exception as exc:
            print(f"ERROR ({exc})")
            errors += 1

    # Remove deleted files
    removed = 0
    for path in to_delete:
        if _is_protected(path):
            continue
        dest = PROJECT_ROOT / path
        if dest.exists():
            try:
                dest.unlink()
                print(f"  Removed: {path}")
                removed += 1
            except Exception:
                pass

    # Update local VERSION
    VERSION_FILE.write_text(server_version + "\n")

    print()
    print("=" * 50)
    print(f"  Updated  : {updated} files")
    print(f"  Removed  : {removed} files")
    print(f"  Skipped  : {skipped} (protected)")
    if errors:
        print(f"  Errors   : {errors}")
    print(f"  Version  : {server_version}")
    print("=" * 50)

    return updated + removed


def restart_services():
    """Restart BizNode services after update."""
    import subprocess

    print("\n  Restarting services...")

    # Kill existing bot
    subprocess.run(
        'powershell -Command "Get-Process python -ErrorAction SilentlyContinue | '
        'ForEach-Object { $id=$_.Id; '
        '$cmd=(Get-CimInstance Win32_Process -Filter \\"ProcessId=$id\\").CommandLine; '
        'if ($cmd -match \'bot_runner|run_bot\') { Stop-Process -Id $id -Force } }"',
        shell=True, capture_output=True,
    )

    time.sleep(2)

    # Restart bot
    import subprocess as sp
    sp.Popen(
        ["python", "bot_runner.py"],
        cwd=str(PROJECT_ROOT),
        stdout=open(str(PROJECT_ROOT / "bot.log"), "a"),
        stderr=sp.STDOUT,
        creationflags=sp.DETACHED_PROCESS | sp.CREATE_NEW_PROCESS_GROUP,
    )
    print("  Bot restarted.")

    # Restart dashboard if running
    subprocess.run(
        'powershell -Command "Get-Process python -ErrorAction SilentlyContinue | '
        'ForEach-Object { $id=$_.Id; '
        '$cmd=(Get-CimInstance Win32_Process -Filter \\"ProcessId=$id\\").CommandLine; '
        'if ($cmd -match \'run_dashboard|launcher\') { Stop-Process -Id $id -Force } }"',
        shell=True, capture_output=True,
    )
    time.sleep(1)
    sp.Popen(
        ["python", "run_dashboard.py"],
        cwd=str(PROJECT_ROOT),
        stdout=open(str(PROJECT_ROOT / "dashboard.log"), "a"),
        stderr=sp.STDOUT,
        creationflags=sp.DETACHED_PROCESS | sp.CREATE_NEW_PROCESS_GROUP,
    )
    print("  Dashboard restarted.")
    print()


def main():
    parser = argparse.ArgumentParser(description="BizNode Delta Updater")
    parser.add_argument("--check", action="store_true",
                        help="Check for updates only (don't apply)")
    parser.add_argument("--force", action="store_true",
                        help="Update even if versions match")
    parser.add_argument("--restart", action="store_true",
                        help="Restart services after update")
    args = parser.parse_args()

    print()
    print("=" * 50)
    print("  BizNode Updater")
    print("=" * 50)

    local_version = get_local_version()
    print(f"\n  Computing local checksums...")
    local_checksums = compute_local_checksums()
    print(f"  Found {len(local_checksums)} local files")

    diff = check_update(local_version, local_checksums)

    if not diff.get("update_needed") and not args.force:
        print("\n  Your BizNode is up to date!")
        return

    to_update = diff.get("to_update", [])
    to_delete = diff.get("to_delete", [])
    print(f"\n  Update available: {len(to_update)} changed, {len(to_delete)} removed")

    if args.check:
        print("\n  Run without --check to apply updates.")
        if to_update:
            print("\n  Changed files:")
            for f in to_update[:20]:
                print(f"    {f['path']} ({f['size']} bytes)")
            if len(to_update) > 20:
                print(f"    ... and {len(to_update) - 20} more")
        return

    applied = apply_update(diff)

    if applied > 0 and args.restart:
        restart_services()
    elif applied > 0:
        print("\n  Run with --restart to restart services, or restart manually.")

    print("  Done.\n")


if __name__ == "__main__":
    main()
