#!/usr/bin/env python3
"""
Daneshyar - Remote Deploy
Run: python scripts/deploy.py

Requires: pip install paramiko
"""

from __future__ import annotations

import getpass
import sys
import time
import zipfile
from pathlib import Path

# --- CONFIG ------------------------------------------------------------------
SERVER_HOST = "91.107.131.171"
SERVER_PORT = 2435
SSH_USER    = "root"
SSH_PASS    = "V9r!k2Qp7#Lm3@tX8sN5$zHu"          # put your password here
APP_DIR     = "/var/www/html"
PHP_VERSION = "8.3"
# -----------------------------------------------------------------------------

ROOT = Path(__file__).resolve().parents[1]


def _require_paramiko() -> None:
    try:
        import paramiko  # noqa: F401
    except ImportError:
        print("ERROR: paramiko not installed. Run: pip install paramiko")
        sys.exit(1)


def _connect(password: str):
    import paramiko
    client = paramiko.SSHClient()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    print(f"Connecting to {SSH_USER}@{SERVER_HOST}:{SERVER_PORT} ...")
    client.connect(SERVER_HOST, port=SERVER_PORT, username=SSH_USER, password=password, timeout=30)
    client.get_transport().set_keepalive(30)  # send keepalive every 30s to prevent idle disconnect
    print("Connected.\n")
    return client


def _run(client, cmd: str, *, check: bool = True) -> tuple[int, str, str]:
    print(f"  -> {cmd}")
    stdin, stdout, stderr = client.exec_command(cmd, get_pty=True)
    stdin.close()
    out_lines: list[str] = []
    for line in iter(stdout.readline, ""):
        print(f"     {line}", end="")
        out_lines.append(line)
    code = stdout.channel.recv_exit_status()
    err  = stderr.read().decode("utf-8", errors="replace")
    if check and code != 0:
        print(f"\n[FAILED] exit code {code}")
        if err.strip():
            print(err)
        sys.exit(code)
    return code, "".join(out_lines), err


def _cmd_exists(client, cmd: str) -> bool:
    code, _, _ = _run(client, f"which {cmd} 2>/dev/null", check=False)
    return code == 0


# -- Steps --------------------------------------------------------------------

def step_status(client) -> None:
    print("=== Server Status ===")
    for cmd, label in [
        (f"php{PHP_VERSION}", f"PHP {PHP_VERSION}"),
        ("apache2",  "Apache2"),
        ("mysql",    "MySQL"),
        ("composer", "Composer"),
        ("node",     "Node.js"),
    ]:
        code, _, _ = _run(client, f"which {cmd} 2>/dev/null", check=False)
        status = "[OK]" if code == 0 else "[!!] missing"
        print(f"  {label:<12} {status}")
    code, out, _ = _run(client, f"test -d {APP_DIR} && echo exists || echo missing", check=False)
    print(f"  App dir:     {out.strip()}")
    print()


def step_setup(client) -> None:
    print("=== System Setup ===\n")

    print("[1/8] Updating packages...")
    _run(client, "export DEBIAN_FRONTEND=noninteractive && apt-get update -qq")

    print("[2/8] Installing base tools...")
    _run(client, "apt-get install -y -qq software-properties-common ca-certificates curl unzip git rsync gnupg2")

    print("[3/8] Adding PHP PPA...")
    _run(client, "add-apt-repository -y ppa:ondrej/php 2>&1 | tail -3 || true")
    _run(client, "apt-get update -qq")

    print(f"[4/8] Installing PHP {PHP_VERSION} + extensions...")
    exts = "cli common mysql mbstring xml curl zip gd bcmath intl fileinfo tokenizer ctype dom pdo"
    pkgs = " ".join(f"php{PHP_VERSION}-{e}" for e in exts.split())
    _run(client, f"apt-get install -y -qq php{PHP_VERSION} libapache2-mod-php{PHP_VERSION} {pkgs}")

    print("[5/8] Installing Apache2...")
    _run(client, "apt-get install -y -qq apache2")
    _run(client, "a2enmod rewrite headers expires deflate ssl")
    # Switch Apache to PHP 8.3 module (disable any older version first)
    _run(client, "a2dismod php8.1 php8.2 2>/dev/null || true", check=False)
    _run(client, f"a2enmod php{PHP_VERSION}")
    _run(client, "systemctl enable apache2 && systemctl start apache2")

    print("[6/8] Installing MySQL...")
    _run(client, "apt-get install -y -qq mysql-server")
    _run(client, "systemctl enable mysql && systemctl start mysql")

    print("[7/8] Installing Composer...")
    if not _cmd_exists(client, "composer"):
        _run(client, "curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php")
        _run(client, f"php{PHP_VERSION} /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer")
        _run(client, "rm -f /tmp/composer-setup.php")
    else:
        print("  Already installed - skipping.")

    print("[8/8] Installing Node.js 20...")
    if not _cmd_exists(client, "node"):
        _run(client, "curl -fsSL https://deb.nodesource.com/setup_20.x | bash -")
        _run(client, "apt-get install -y -qq nodejs")
    else:
        print("  Already installed - skipping.")

    # Set PHP 8.3 as the default php command
    _run(client, f"update-alternatives --set php /usr/bin/php{PHP_VERSION}", check=False)
    _run(client, f"update-alternatives --set php-config /usr/bin/php-config{PHP_VERSION}", check=False)
    _run(client, f"update-alternatives --set phpize /usr/bin/phpize{PHP_VERSION}", check=False)

    _run(client, f"mkdir -p {APP_DIR}")
    print("\n[OK] System setup complete.\n")


def step_configure_apache(client) -> None:
    print("=== Apache Configuration ===\n")
    local_conf = ROOT / "scripts" / "configure_apache.py"
    if local_conf.exists():
        sftp = client.open_sftp()
        sftp.put(str(local_conf), "/tmp/configure_apache.py")
        sftp.close()
        _run(client, f"python3 /tmp/configure_apache.py --app-dir {APP_DIR} --server-name {SERVER_HOST}")
    else:
        print("  WARNING: configure_apache.py not found - skipping.")
    print()


def step_setup_mysql(client) -> None:
    print("=== Database Setup ===")
    db_name = input("  Database name [daneshyar]: ").strip() or "daneshyar"
    db_user = input("  DB username   [daneshyar]: ").strip() or "daneshyar"
    db_pass = getpass.getpass("  DB password (required, not empty): ")
    if not db_pass:
        print("  ERROR: DB password cannot be empty.")
        sys.exit(1)

    # Write SQL via SFTP to avoid all shell quoting issues
    sql = "\n".join([
        f"CREATE DATABASE IF NOT EXISTS {db_name} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;",
        f"CREATE USER IF NOT EXISTS '{db_user}'@'localhost' IDENTIFIED BY '{db_pass}';",
        f"GRANT ALL PRIVILEGES ON {db_name}.* TO '{db_user}'@'localhost';",
        "FLUSH PRIVILEGES;",
    ])
    sftp = client.open_sftp()
    with sftp.file("/tmp/db_setup.sql", "w") as f:
        f.write(sql)
    sftp.close()
    _run(client, "mysql -u root < /tmp/db_setup.sql")
    _run(client, "rm -f /tmp/db_setup.sql")

    print(f"\n[OK] Database ready.")
    print(f"\n  Add to {APP_DIR}/.env:")
    print(f"    DB_DATABASE={db_name}")
    print(f"    DB_USERNAME={db_user}")
    print(f"    DB_PASSWORD={db_pass}\n")


def step_upload(client) -> None:
    print("=== Upload ===\n")

    if not (ROOT / "public" / "build" / "manifest.json").exists():
        print("WARNING: public/build/manifest.json not found.")
        ans = input("  Run 'python scripts/build.py' first. Continue anyway? [y/N]: ").strip().lower()
        if ans != "y":
            sys.exit(0)

    ts       = time.strftime("%Y%m%d-%H%M%S")
    zip_path = ROOT / "build" / f"daneshyar-{ts}.zip"
    zip_path.parent.mkdir(exist_ok=True)

    SKIP_DIRS  = {".git", ".idea", ".vscode", "node_modules", "build",
                  "storage/logs", "storage/framework/cache",
                  "storage/framework/sessions", "storage/framework/views", "__pycache__"}
    SKIP_FILES = {".env", ".DS_Store", "Thumbs.db"}
    SKIP_EXT   = {".zip", ".pyc"}

    print(f"  Packaging -> {zip_path.name}")
    count = 0
    with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=6) as zf:
        for p in ROOT.rglob("*"):
            if p.is_dir():
                continue
            rel = p.relative_to(ROOT).as_posix()
            if any(rel == ex or rel.startswith(ex + "/") for ex in SKIP_DIRS):
                continue
            if p.name in SKIP_FILES or p.suffix in SKIP_EXT:
                continue
            zf.write(p, rel)
            count += 1
    mb = zip_path.stat().st_size / 1024 / 1024
    print(f"  {count} files, {mb:.1f} MB")

    remote_zip = f"/tmp/{zip_path.name}"
    print(f"\n  Uploading -> server:{remote_zip}")
    sftp = client.open_sftp()

    def _progress(transferred: int, total: int) -> None:
        pct = (transferred / total) * 100
        bar = "#" * int(pct / 5) + "." * (20 - int(pct / 5))
        print(f"\r  [{bar}] {pct:.0f}%", end="", flush=True)

    sftp.put(str(zip_path), remote_zip, callback=_progress)
    sftp.close()
    print()

    print(f"\n  Extracting to {APP_DIR}...")
    _run(client, f"mkdir -p {APP_DIR}")
    _run(client, f"unzip -o {remote_zip} -d {APP_DIR} > /dev/null")
    _run(client, f"rm -f {remote_zip}")
    _run(client, f"chown -R www-data:www-data {APP_DIR} || true")
    _run(client, f"find {APP_DIR} -type f -exec chmod 644 {{}} \\;")
    _run(client, f"find {APP_DIR} -type d -exec chmod 755 {{}} \\;")
    _run(client, f"chmod -R 775 {APP_DIR}/storage {APP_DIR}/bootstrap/cache 2>/dev/null || true")
    _run(client, f"chmod +x {APP_DIR}/artisan 2>/dev/null || true")

    print(f"\n[OK] Upload complete.\n")


def step_post_deploy(client) -> None:
    print("=== Post-Deploy ===\n")
    cwd = APP_DIR
    php = f"php{PHP_VERSION}"

    # [0] Check .env exists
    print("[0/7] Checking .env...")
    code, _, _ = _run(client, f"test -f {cwd}/.env", check=False)
    if code != 0:
        print(f"  ERROR: {cwd}/.env not found!")
        print(f"  Create it:")
        print(f"    ssh {SSH_USER}@{SERVER_HOST} -p {SERVER_PORT}")
        print(f"    cp {cwd}/.env.example {cwd}/.env && nano {cwd}/.env")
        sys.exit(1)
    print("  .env found.")

    # Show DB config so user can verify before migrating
    print("\n  DB config from .env:")
    _run(client, f"grep '^DB_' {cwd}/.env | grep -v PASSWORD || true", check=False)
    _, db_pass_line, _ = _run(client, f"grep '^DB_PASSWORD' {cwd}/.env || true", check=False)
    if db_pass_line.strip():
        print("  DB_PASSWORD=***")

    # [1] Permissions
    print("\n[1/7] Setting permissions...")
    dirs = [f"{cwd}/storage/logs", f"{cwd}/storage/framework/cache",
            f"{cwd}/storage/framework/sessions", f"{cwd}/storage/framework/views",
            f"{cwd}/bootstrap/cache"]
    _run(client, f"mkdir -p {' '.join(dirs)}")
    _run(client, f"chown -R www-data:www-data {cwd}/storage {cwd}/bootstrap/cache")
    _run(client, f"chmod -R 775 {cwd}/storage {cwd}/bootstrap/cache")

    # [2] Composer
    print("\n[2/7] Composer install...")
    _run(client, f"cd {cwd} && COMPOSER_ALLOW_SUPERUSER=1 {php} /usr/local/bin/composer install --no-dev --optimize-autoloader --no-interaction")
    _run(client, f"chown -R www-data:www-data {cwd}")

    # [3] App key
    print("\n[3/7] App key...")
    _run(client, f"cd {cwd} && {php} artisan key:generate --force 2>&1 | grep -v already || true", check=False)

    # [4] Test DB connection — write PHP file via SFTP to avoid all shell quoting issues
    print("\n[4/7] Testing database connection...")
    db_test_php = r"""<?php
function env_get($file, $key) {
    foreach (file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
        $line = trim($line);
        if ($line === '' || $line[0] === '#') continue;
        if (strpos($line, $key . '=') === 0) {
            $val = substr($line, strlen($key) + 1);
            return trim($val, "\"'");
        }
    }
    return '';
}
$f = '/var/www/html/.env';
$host = env_get($f, 'DB_HOST');
$port = (int)(env_get($f, 'DB_PORT') ?: 3306);
$name = env_get($f, 'DB_DATABASE');
$user = env_get($f, 'DB_USERNAME');
$pass = env_get($f, 'DB_PASSWORD');

$sock = @fsockopen($host, $port, $errno, $errstr, 10);
if (!$sock) { echo "TCP FAIL: $errno $errstr\n"; exit(2); }
fclose($sock);
echo "TCP OK\n";

try {
    $dsn = "mysql:host=$host;port=$port;dbname=$name;connect_timeout=10";
    new PDO($dsn, $user, $pass, [PDO::ATTR_TIMEOUT => 10]);
    echo "DB OK\n";
} catch (Exception $ex) {
    echo "DB FAIL: " . $ex->getMessage() . "\n";
    exit(1);
}
"""
    sftp = client.open_sftp()
    with sftp.file("/tmp/db_test.php", "w") as f:
        f.write(db_test_php)
    sftp.close()

    code, out, err = _run(client, f"timeout 20 {php} /tmp/db_test.php 2>&1; rm -f /tmp/db_test.php", check=False)
    if code != 0 or "FAIL" in out or "DB OK" not in out:
        print("  ERROR: Database connection failed!")
        print(f"  {out.strip()}")
        print(f"\n  Common causes:")
        print(f"    - DB host firewall: whitelist server IP {SERVER_HOST}")
        print(f"    - Wrong DB_PORT / DB_USERNAME / DB_PASSWORD / DB_DATABASE in .env")
        print(f"\n  Edit .env:")
        print(f"    ssh {SSH_USER}@{SERVER_HOST} -p {SERVER_PORT}")
        print(f"    nano {cwd}/.env")
        sys.exit(1)
    print(f"  {out.strip()}")

    # [5] Migrate
    print("\n[5/7] Running migrations (this may take 1-2 minutes)...")
    _run(client, f"cd {cwd} && {php} artisan migrate --force")

    # Create Laravel system tables not covered by app migrations
    print("  Creating sessions/cache/jobs tables if missing...")
    system_sql = """
CREATE TABLE IF NOT EXISTS sessions (
    id VARCHAR(255) NOT NULL, user_id BIGINT UNSIGNED NULL,
    ip_address VARCHAR(45) NULL, user_agent TEXT NULL,
    payload LONGTEXT NOT NULL, last_activity INT NOT NULL,
    PRIMARY KEY (id), INDEX sessions_user_id_index (user_id),
    INDEX sessions_last_activity_index (last_activity)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS cache (
    `key` VARCHAR(255) NOT NULL, value MEDIUMTEXT NOT NULL,
    expiration INT NOT NULL, PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS cache_locks (
    `key` VARCHAR(255) NOT NULL, owner VARCHAR(255) NOT NULL,
    expiration INT NOT NULL, PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS jobs (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, queue VARCHAR(255) NOT NULL,
    payload LONGTEXT NOT NULL, attempts TINYINT UNSIGNED NOT NULL,
    reserved_at INT UNSIGNED NULL, available_at INT UNSIGNED NOT NULL,
    created_at INT UNSIGNED NOT NULL, PRIMARY KEY (id),
    INDEX jobs_queue_index (queue)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
"""
    # Read DB credentials from .env to build mysql connection
    _, env_out, _ = _run(client, f"grep '^DB_' {cwd}/.env", check=False)
    db = {}
    for line in env_out.splitlines():
        k, _, v = line.partition('=')
        db[k.strip()] = v.strip().strip('"\'')
    sftp = client.open_sftp()
    with sftp.file("/tmp/sys_tables.sql", "w") as f:
        f.write(system_sql)
    sftp.close()
    _run(client,
        f"mysql -h {db.get('DB_HOST','localhost')} -P {db.get('DB_PORT','3306')} "
        f"-u {db.get('DB_USERNAME','root')} -p{db.get('DB_PASSWORD','')} "
        f"{db.get('DB_DATABASE','')} < /tmp/sys_tables.sql 2>&1 | grep -v 'Warning' || true",
        check=False)
    _run(client, "rm -f /tmp/sys_tables.sql")

    # [6] Cache
    print("\n[6/7] Caching config/routes/views...")
    _run(client, f"cd {cwd} && {php} artisan config:clear")
    _run(client, f"cd {cwd} && {php} artisan config:cache")
    _run(client, f"cd {cwd} && {php} artisan route:cache")
    _run(client, f"cd {cwd} && {php} artisan view:cache")
    _run(client, f"cd {cwd} && {php} artisan storage:link", check=False)

    # [7] Reload Apache
    print("\n[7/7] Reloading Apache...")
    _run(client, "systemctl reload apache2")

    sep = "-" * 55
    print(f"\n{sep}")
    print("  DEPLOY COMPLETE")
    print(sep)
    print(f"\n  Site:  http://{SERVER_HOST}/")
    print(f"  Login: http://{SERVER_HOST}/login")
    print(f"\n  Logs:")
    print(f"    /var/log/apache2/daneshyar_error.log")
    print(f"    /var/log/apache2/daneshyar_access.log")
    print(sep)


# -- Main ---------------------------------------------------------------------

def main() -> None:
    _require_paramiko()

    print("=" * 55)
    print("  Daneshyar - Full Server Deploy")
    print("=" * 55)
    print(f"\n  Target: {SSH_USER}@{SERVER_HOST}:{SERVER_PORT}")
    print(f"  App:    {APP_DIR}\n")

    password = SSH_PASS if SSH_PASS else getpass.getpass(f"SSH password for {SSH_USER}@{SERVER_HOST}: ")
    client   = _connect(password)

    def ask(question: str) -> bool:
        return input(f"\n{question} [Y/n]: ").strip().lower() != "n"

    try:
        step_status(client)

        if ask("Run system setup? (install PHP/Apache/MySQL/Composer/Node)"):
            step_setup(client)

        if ask("Configure Apache?"):
            step_configure_apache(client)

        if ask("Set up MySQL database?"):
            step_setup_mysql(client)
            print("\nNOW: SSH in and create your .env file before continuing.")
            print(f"  ssh {SSH_USER}@{SERVER_HOST}")
            print(f"  cp {APP_DIR}/.env.example {APP_DIR}/.env")
            print(f"  nano {APP_DIR}/.env")
            input("\nPress Enter when .env is ready...")

        if ask("Upload project files?"):
            step_upload(client)

        if ask("Run post-deploy? (composer install, migrate, cache, reload Apache)"):
            step_post_deploy(client)

    finally:
        client.close()
        print("SSH connection closed.")


if __name__ == "__main__":
    main()
