#!/usr/bin/env python3
"""
Deploy script — run locally to push a new release to the server.

Usage:
    python deploy.py            # normal deploy
    python deploy.py --dry-run  # show what would happen without touching the server

Reads DEPLOY_SSH, DEPLOY_PATH, DEPLOY_SSH_PORT, DEPLOY_SSH_KEY from .env
"""

import os
import sys
import zipfile
import tempfile
import subprocess
import shutil
from dotenv import load_dotenv

load_dotenv()

DEPLOY_SSH = os.environ.get("DEPLOY_SSH", "")
DEPLOY_PATH = os.environ.get("DEPLOY_PATH", "")
SSH_PORT = os.environ.get("DEPLOY_SSH_PORT", "22")
SSH_KEY = os.environ.get("DEPLOY_SSH_KEY", "")

PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
ZIP_NAME = "science_gr2_deploy.zip"

EXCLUDE_DIRS = {
    "__pycache__", ".git", "_Data", "Book",
    "node_modules", ".venv", "venv",
}
EXCLUDE_FILES = {
    ".env", "deploy.py", "debug_run.py",
    "debug_out.txt", "debug_err.txt", ".gitignore",
}
EXCLUDE_EXTS = {".pyc", ".pyo", ".zip"}


def log(msg):
    print(f"  {msg}")


def header(msg):
    print(f"\n{'='*60}")
    print(f"  {msg}")
    print(f"{'='*60}")


def should_exclude(path, name):
    full = os.path.join(path, name)
    if os.path.isdir(full):
        return name in EXCLUDE_DIRS
    if name in EXCLUDE_FILES:
        return True
    _, ext = os.path.splitext(name)
    return ext in EXCLUDE_EXTS


def build_zip(zip_path):
    header("1/5  Building zip archive")
    count = 0
    with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
        for root, dirs, files in os.walk(PROJECT_DIR):
            dirs[:] = [d for d in dirs if not should_exclude(root, d)]
            for f in files:
                if should_exclude(root, f):
                    continue
                full = os.path.join(root, f)
                rel = os.path.relpath(full, PROJECT_DIR)
                zf.write(full, rel)
                count += 1
    size_mb = os.path.getsize(zip_path) / (1024 * 1024)
    log(f"Packed {count} files ({size_mb:.1f} MB) -> {zip_path}")
    return count


def ssh_base():
    parts = ["ssh"]
    if SSH_KEY:
        parts += ["-i", os.path.expanduser(SSH_KEY)]
    parts += ["-p", str(SSH_PORT), "-o", "StrictHostKeyChecking=no"]
    parts += [DEPLOY_SSH]
    return parts


def run_ssh(cmd):
    full = ssh_base() + [cmd]
    result = subprocess.run(full, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"  SSH ERROR: {result.stderr.strip()}")
        return False
    if result.stdout.strip():
        print(f"  {result.stdout.strip()}")
    return True


def scp(local, remote):
    parts = ["scp"]
    if SSH_KEY:
        parts += ["-i", os.path.expanduser(SSH_KEY)]
    parts += ["-P", str(SSH_PORT), "-o", "StrictHostKeyChecking=no"]
    parts += [local, f"{DEPLOY_SSH}:{remote}"]
    result = subprocess.run(parts, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"  SCP ERROR: {result.stderr.strip()}")
        return False
    return True


def deploy(dry_run=False, with_env=False):
    if not DEPLOY_SSH or not DEPLOY_PATH:
        print("ERROR: Set DEPLOY_SSH and DEPLOY_PATH in .env first.")
        print(f"  DEPLOY_SSH  = {DEPLOY_SSH or '(empty)'}")
        print(f"  DEPLOY_PATH = {DEPLOY_PATH or '(empty)'}")
        sys.exit(1)

    header(f"Deploying to {DEPLOY_SSH}:{DEPLOY_PATH}")
    if dry_run:
        print("  ** DRY RUN — no changes will be made **\n")
    if with_env:
        print("  ** .env will be pushed to server (credentials will update) **\n")

    tmpdir = tempfile.mkdtemp()
    zip_path = os.path.join(tmpdir, ZIP_NAME)

    try:
        build_zip(zip_path)

        if dry_run:
            header("2/6  (dry-run) SCP zip to server")
            log(f"Would scp {ZIP_NAME} -> {DEPLOY_SSH}:/tmp/{ZIP_NAME}")
            if with_env:
                header("3/6  (dry-run) SCP .env to server")
                log(f"Would scp .env -> {DEPLOY_SSH}:{DEPLOY_PATH}/.env")
            header("4/6  (dry-run) Unzip on server")
            log(f"Would unzip to {DEPLOY_PATH} (overwrite existing)")
            header("5/6  (dry-run) Restart containers")
            log("Would run: docker compose down && docker compose up -d --build --no-cache")
            header("6/6  (dry-run) Cleanup")
            log("Would remove /tmp/science_gr2_deploy.zip")
            header("DRY RUN COMPLETE")
            return

        header("2/6  Uploading zip to server")
        remote_zip = f"/tmp/{ZIP_NAME}"
        if not scp(zip_path, remote_zip):
            sys.exit(1)
        log("Upload complete")

        if with_env:
            header("3/6  Uploading .env to server (credentials update)")
            env_local = os.path.join(PROJECT_DIR, ".env")
            if os.path.exists(env_local):
                if not scp(env_local, f"{DEPLOY_PATH}/.env"):
                    sys.exit(1)
                log(".env uploaded — credentials will update on restart")
            else:
                print("  WARNING: .env not found locally, skipping")
        else:
            header("3/6  Skipping .env (use --with-env to push credentials)")

        header("4/6  Unzipping on server (overwrite existing files)")
        cmds = [
            f"mkdir -p {DEPLOY_PATH}",
            f"cd {DEPLOY_PATH} && unzip -o {remote_zip} -d {DEPLOY_PATH}",
        ]
        for cmd in cmds:
            if not run_ssh(cmd):
                sys.exit(1)
        log("Files extracted and overwritten")

        header("5/6  Rebuilding and restarting containers")
        compose_cmd = (
            f"cd {DEPLOY_PATH} && "
            f"docker compose down && "
            f"docker compose build --no-cache && "
            f"docker compose up -d"
        )
        if not run_ssh(compose_cmd):
            sys.exit(1)
        log("Containers restarted with fresh build")

        header("6/6  Cleaning up")
        run_ssh(f"rm -f {remote_zip}")
        log("Remote temp zip removed")

        header("DEPLOY COMPLETE")
        print(f"  App is live at https://gr2.thinkera.academy/science")
        print(f"  Deployed to: {DEPLOY_PATH}")
        if with_env:
            print(f"  Credentials updated from .env")
        print()

    finally:
        shutil.rmtree(tmpdir, ignore_errors=True)


if __name__ == "__main__":
    dry = "--dry-run" in sys.argv or "-n" in sys.argv
    with_env = "--with-env" in sys.argv or "-e" in sys.argv
    deploy(dry_run=dry, with_env=with_env)
