#!/usr/bin/env python3
"""
Exports the Golder master database and every tenant database referenced by the
customers table into a single zip bundle.

Requirements on the VPS:
  - Python 3.9+
  - mysql-connector-python
  - mysqldump binary available in PATH

Example:
  python tools/golder_db_bundle_export.py \
    --host 127.0.0.1 \
    --port 3306 \
    --master-user goldermaster \
    --master-pass 'Mmty2676*' \
    --master-db golder_master \
    --output-dir backups
"""

from __future__ import annotations

import argparse
import json
import os
import shutil
import subprocess
import sys
import tempfile
import zipfile
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import List, Optional

import mysql.connector


@dataclass
class TenantDb:
    mysql_db_name: str
    mysql_db_user: str
    mysql_db_pass: str


DEFAULT_MASTER_DB_CANDIDATES = [
    "golder_master",
    "goldermaster",
    "Goldr",
]


def require_binary(binary_name: str) -> None:
    if shutil.which(binary_name):
        return
    print(f"HATA: '{binary_name}' PATH içinde bulunamadı.", file=sys.stderr)
    sys.exit(1)


def fetch_tenant_dbs(
    host: str,
    port: int,
    master_user: str,
    master_pass: str,
    master_db: str,
) -> List[TenantDb]:
    connection = mysql.connector.connect(
        host=host,
        port=port,
        user=master_user,
        password=master_pass,
        database=master_db,
    )
    try:
        cursor = connection.cursor(dictionary=True)
        cursor.execute(
            """
            SELECT mysql_db_name, mysql_db_user, mysql_db_pass
            FROM customers
            WHERE mysql_db_name IS NOT NULL
              AND mysql_db_name <> ''
              AND mysql_db_user IS NOT NULL
              AND mysql_db_user <> ''
            ORDER BY id
            """
        )
        rows = cursor.fetchall()
        return [TenantDb(**row) for row in rows]
    finally:
        connection.close()


def resolve_master_db(
    host: str,
    port: int,
    master_user: str,
    master_pass: str,
    requested_master_db: Optional[str],
) -> str:
    connection = mysql.connector.connect(
        host=host,
        port=port,
        user=master_user,
        password=master_pass,
    )
    try:
        cursor = connection.cursor()

        def exists(db_name: str) -> bool:
            cursor.execute("SHOW DATABASES LIKE %s", (db_name,))
            return cursor.fetchone() is not None

        detected = [name for name in DEFAULT_MASTER_DB_CANDIDATES if exists(name)]

        if requested_master_db:
            if exists(requested_master_db):
                return requested_master_db
            if detected:
                raise RuntimeError(
                    "Istenen master DB bulunamadi: "
                    f"{requested_master_db}. Bulunan adaylar: {', '.join(detected)}"
                )
            raise RuntimeError(
                "Istenen master DB bulunamadi: "
                f"{requested_master_db}. --master-db degerini kontrol edin."
            )

        if detected:
            selected = detected[0]
            print(f"Master DB otomatik tespit edildi: {selected}")
            return selected

        raise RuntimeError(
            "Master DB otomatik tespit edilemedi. "
            "--master-db ile veritabani adini acikca belirtin."
        )
    finally:
        connection.close()


def run_dump(
    host: str,
    port: int,
    user: str,
    password: str,
    database: str,
    output_file: Path,
) -> None:
    env = os.environ.copy()
    env["MYSQL_PWD"] = password
    cmd = [
        "mysqldump",
        f"--host={host}",
        f"--port={port}",
        f"--user={user}",
        "--default-character-set=utf8mb4",
        "--single-transaction",
        "--routines",
        "--events",
        "--triggers",
        "--hex-blob",
        "--skip-comments",
        "--databases",
        database,
    ]
    with output_file.open("wb") as handle:
        result = subprocess.run(
            cmd,
            env=env,
            stdout=handle,
            stderr=subprocess.PIPE,
            check=False,
        )
    if result.returncode != 0:
        output_file.unlink(missing_ok=True)
        stderr = result.stderr.decode("utf-8", errors="replace").strip()
        raise RuntimeError(f"{database} dump alinamadi: {stderr}")


def write_manifest(
    manifest_file: Path,
    host: str,
    port: int,
    master_db: str,
    tenants: List[TenantDb],
    dump_files: List[str],
) -> None:
    manifest = {
        "created_at_utc": datetime.now(timezone.utc).isoformat(),
        "source": {
            "host": host,
            "port": port,
            "master_db": master_db,
        },
        "master_db": master_db,
        "tenant_count": len(tenants),
        "tenants": [asdict(item) for item in tenants],
        "dump_files": dump_files,
    }
    manifest_file.write_text(
        json.dumps(manifest, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Export Golder master ve tenant DB'lerini tek zip paketine alir."
    )
    parser.add_argument("--host", default="127.0.0.1")
    parser.add_argument("--port", type=int, default=3306)
    parser.add_argument("--master-user", required=True)
    parser.add_argument("--master-pass", required=True)
    parser.add_argument(
        "--master-db",
        default=None,
        help="Master veritabani adi. Verilmezse bilinen aday adlar otomatik denenir.",
    )
    parser.add_argument("--output-dir", default="backups")
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    require_binary("mysqldump")

    output_dir = Path(args.output_dir).resolve()
    output_dir.mkdir(parents=True, exist_ok=True)

    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    bundle_name = f"golder_backup_{timestamp}.zip"
    bundle_path = output_dir / bundle_name

    print("Master veritabani tespit ediliyor...")
    master_db = resolve_master_db(
        host=args.host,
        port=args.port,
        master_user=args.master_user,
        master_pass=args.master_pass,
        requested_master_db=args.master_db,
    )

    print(f"Master veritabanina baglaniliyor: {master_db}")
    tenants = fetch_tenant_dbs(
        host=args.host,
        port=args.port,
        master_user=args.master_user,
        master_pass=args.master_pass,
        master_db=master_db,
    )
    print(f"{len(tenants)} tenant veritabani bulundu.")

    with tempfile.TemporaryDirectory(prefix="golder_export_") as temp_dir:
        temp_path = Path(temp_dir)
        dumps_dir = temp_path / "dumps"
        dumps_dir.mkdir(parents=True, exist_ok=True)

        dump_files: List[str] = []

        master_dump = dumps_dir / f"{master_db}.sql"
        print(f"Master dump aliniyor: {master_db}")
        run_dump(
            host=args.host,
            port=args.port,
            user=args.master_user,
            password=args.master_pass,
            database=master_db,
            output_file=master_dump,
        )
        dump_files.append(master_dump.name)

        for index, tenant in enumerate(tenants, start=1):
            dump_file = dumps_dir / f"{tenant.mysql_db_name}.sql"
            print(
                f"[{index}/{len(tenants)}] Tenant dump aliniyor: {tenant.mysql_db_name}"
            )
            run_dump(
                host=args.host,
                port=args.port,
                user=tenant.mysql_db_user,
                password=tenant.mysql_db_pass,
                database=tenant.mysql_db_name,
                output_file=dump_file,
            )
            dump_files.append(dump_file.name)

        manifest_file = temp_path / "manifest.json"
        write_manifest(
            manifest_file=manifest_file,
            host=args.host,
            port=args.port,
            master_db=master_db,
            tenants=tenants,
            dump_files=dump_files,
        )

        with zipfile.ZipFile(bundle_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
            zf.write(manifest_file, arcname="manifest.json")
            for sql_file_name in dump_files:
                sql_path = dumps_dir / sql_file_name
                zf.write(sql_path, arcname=f"dumps/{sql_file_name}")

    print(f"Tamamlandi. Paket dosyasi: {bundle_path}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
