"""YakPanel - Database service (MySQL creation via CLI; PostgreSQL/MongoDB/Redis panel records only)"""
import os
import shutil
import subprocess
from datetime import datetime
from app.core.config import get_runtime_config


def get_mysql_root() -> str | None:
    """Get MySQL root password from config (key: mysql_root)"""
    cfg = get_runtime_config()
    return cfg.get("mysql_root") or None


def _run_mysql(sql: str, root_pw: str) -> tuple[str, str]:
    """Run mysql with SQL. Uses MYSQL_PWD to avoid password in argv."""
    env = os.environ.copy()
    env["MYSQL_PWD"] = root_pw
    try:
        r = subprocess.run(
            ["mysql", "-u", "root", "-e", sql],
            capture_output=True,
            text=True,
            timeout=10,
            env=env,
        )
        return r.stdout or "", r.stderr or ""
    except FileNotFoundError:
        return "", "mysql command not found"
    except subprocess.TimeoutExpired:
        return "", "Timed out"


def create_mysql_database(db_name: str, username: str, password: str) -> tuple[bool, str]:
    """Create MySQL database and user via mysql CLI. Returns (success, message)."""
    root_pw = get_mysql_root()
    if not root_pw:
        return False, "MySQL root password not configured. Set it in Settings."
    for x in (db_name, username):
        if not all(c.isalnum() or c == "_" for c in x):
            return False, f"Invalid characters in name: {x}"
    pw_esc = password.replace("\\", "\\\\").replace("'", "''")
    steps = [
        (f"CREATE DATABASE IF NOT EXISTS `{db_name}`;", "create database"),
        (f"CREATE USER IF NOT EXISTS '{username}'@'localhost' IDENTIFIED BY '{pw_esc}';", "create user"),
        (f"GRANT ALL PRIVILEGES ON `{db_name}`.* TO '{username}'@'localhost';", "grant"),
        ("FLUSH PRIVILEGES;", "flush"),
    ]
    for sql, step in steps:
        out, err = _run_mysql(sql, root_pw)
        if err and "error" in err.lower() and "already exists" not in err.lower():
            return False, f"{step}: {err.strip() or out.strip()}"
    return True, "Database created"


def drop_mysql_database(db_name: str, username: str) -> tuple[bool, str]:
    """Drop MySQL database and user."""
    root_pw = get_mysql_root()
    if not root_pw:
        return False, "MySQL root password not configured"
    for x in (db_name, username):
        if not all(c.isalnum() or c == "_" for c in x):
            return False, f"Invalid characters: {x}"
    steps = [
        (f"DROP DATABASE IF EXISTS `{db_name}`;", "drop database"),
        (f"DROP USER IF EXISTS '{username}'@'localhost';", "drop user"),
        ("FLUSH PRIVILEGES;", "flush"),
    ]
    for sql, step in steps:
        out, err = _run_mysql(sql, root_pw)
        if err and "error" in err.lower():
            return False, f"{step}: {err.strip() or out.strip()}"
    return True, "Database dropped"


def backup_mysql_database(db_name: str, backup_dir: str) -> tuple[bool, str, str | None]:
    """Create mysqldump backup. Returns (success, message, filename)."""
    root_pw = get_mysql_root()
    if not root_pw:
        return False, "MySQL root password not configured", None
    if not all(c.isalnum() or c == "_" for c in db_name):
        return False, "Invalid database name", None
    os.makedirs(backup_dir, exist_ok=True)
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    filename = f"{db_name}_{ts}.sql.gz"
    dest = os.path.join(backup_dir, filename)
    env = os.environ.copy()
    env["MYSQL_PWD"] = root_pw
    try:
        r = subprocess.run(
            f'mysqldump -u root {db_name} | gzip > "{dest}"',
            shell=True,
            env=env,
            timeout=300,
            capture_output=True,
            text=True,
        )
        if r.returncode != 0 or not os.path.isfile(dest):
            return False, r.stderr or r.stdout or "Backup failed", None
        return True, "Backup created", filename
    except subprocess.TimeoutExpired:
        return False, "Timed out", None
    except Exception as e:
        return False, str(e), None


def restore_mysql_database(db_name: str, backup_path: str) -> tuple[bool, str]:
    """Restore MySQL database from .sql.gz backup."""
    root_pw = get_mysql_root()
    if not root_pw:
        return False, "MySQL root password not configured"
    if not all(c.isalnum() or c == "_" for c in db_name):
        return False, "Invalid database name"
    if not os.path.isfile(backup_path):
        return False, "Backup file not found"
    env = os.environ.copy()
    env["MYSQL_PWD"] = root_pw
    try:
        r = subprocess.run(
            f'gunzip -c "{backup_path}" | mysql -u root {db_name}',
            shell=True,
            env=env,
            timeout=300,
            capture_output=True,
            text=True,
        )
        if r.returncode != 0:
            return False, r.stderr or r.stdout or "Restore failed"
        return True, "Restored"
    except subprocess.TimeoutExpired:
        return False, "Timed out"
    except Exception as e:
        return False, str(e)


def change_mysql_password(username: str, new_password: str) -> tuple[bool, str]:
    """Change MySQL user password."""
    root_pw = get_mysql_root()
    if not root_pw:
        return False, "MySQL root password not configured"
    if not all(c.isalnum() or c == "_" for c in username):
        return False, "Invalid username"
    pw_esc = new_password.replace("\\", "\\\\").replace("'", "''")
    sql = f"ALTER USER '{username}'@'localhost' IDENTIFIED BY '{pw_esc}';"
    out, err = _run_mysql(sql, root_pw)
    if err and "error" in err.lower():
        return False, err.strip() or out.strip()
    return True, "Password updated"


def create_postgresql_database(db_name: str, username: str, password: str) -> tuple[bool, str]:
    """Create PostgreSQL database and user via psql (runs as postgres). Returns (success, message)."""
    for x in (db_name, username):
        if not all(c.isalnum() or c == "_" for c in x):
            return False, f"Invalid characters in name: {x}"
    pw_esc = password.replace("'", "''")
    cmds = [
        f"CREATE DATABASE {db_name};",
        f"CREATE USER {username} WITH PASSWORD '{pw_esc}';",
        f"GRANT ALL PRIVILEGES ON DATABASE {db_name} TO {username};",
        f"GRANT ALL ON SCHEMA public TO {username};",
    ]
    for sql in cmds:
        try:
            r = subprocess.run(
                ["sudo", "-u", "postgres", "psql", "-tAc", sql],
                capture_output=True,
                text=True,
                timeout=10,
            )
            if r.returncode != 0:
                err = (r.stderr or r.stdout or "Failed").strip()[:200]
                if "already exists" in err.lower():
                    continue
                return False, err
        except FileNotFoundError:
            return False, "PostgreSQL (psql) not found"
    return True, "Database created"


def drop_postgresql_database(db_name: str, username: str) -> tuple[bool, str]:
    """Drop PostgreSQL database and user."""
    for x in (db_name, username):
        if not all(c.isalnum() or c == "_" for c in x):
            return False, f"Invalid characters: {x}"
    try:
        r = subprocess.run(
            ["sudo", "-u", "postgres", "psql", "-tAc", f"DROP DATABASE IF EXISTS {db_name};"],
            capture_output=True,
            text=True,
            timeout=10,
        )
        if r.returncode != 0:
            return False, (r.stderr or r.stdout or "Failed").strip()[:200]
        r = subprocess.run(
            ["sudo", "-u", "postgres", "psql", "-tAc", f"DROP USER IF EXISTS {username};"],
            capture_output=True,
            text=True,
            timeout=10,
        )
        if r.returncode != 0:
            return False, (r.stderr or r.stdout or "Failed").strip()[:200]
        return True, "Database dropped"
    except FileNotFoundError:
        return False, "PostgreSQL (psql) not found"


def change_postgresql_password(username: str, new_password: str) -> tuple[bool, str]:
    """Change PostgreSQL user password."""
    if not all(c.isalnum() or c == "_" for c in username):
        return False, "Invalid username"
    pw_esc = new_password.replace("'", "''")
    try:
        r = subprocess.run(
            ["sudo", "-u", "postgres", "psql", "-tAc", f"ALTER USER {username} WITH PASSWORD '{pw_esc}';"],
            capture_output=True,
            text=True,
            timeout=10,
        )
        if r.returncode != 0:
            return False, (r.stderr or r.stdout or "Failed").strip()[:200]
        return True, "Password updated"
    except FileNotFoundError:
        return False, "PostgreSQL (psql) not found"


def create_mongodb_database(db_name: str, username: str, password: str) -> tuple[bool, str]:
    """Create MongoDB database and user via mongosh."""
    for x in (db_name, username):
        if not all(c.isalnum() or c == "_" for c in x):
            return False, f"Invalid characters in name: {x}"
    pw_esc = password.replace("\\", "\\\\").replace("'", "\\'")
    js = (
        f"db = db.getSiblingDB('{db_name}'); "
        f"db.createUser({{user: '{username}', pwd: '{pw_esc}', roles: [{{role: 'readWrite', db: '{db_name}'}}]}});"
    )
    try:
        r = subprocess.run(
            ["mongosh", "--quiet", "--eval", js],
            capture_output=True,
            text=True,
            timeout=15,
        )
        if r.returncode != 0:
            err = (r.stderr or r.stdout or "Failed").strip()[:200]
            if "already exists" in err.lower():
                return True, "Database created"
            return False, err
        return True, "Database created"
    except FileNotFoundError:
        return False, "MongoDB (mongosh) not found"


def drop_mongodb_database(db_name: str, username: str) -> tuple[bool, str]:
    """Drop MongoDB database and user."""
    for x in (db_name, username):
        if not all(c.isalnum() or c == "_" for c in x):
            return False, f"Invalid characters: {x}"
    try:
        js = f"db = db.getSiblingDB('{db_name}'); db.dropUser('{username}'); db.dropDatabase();"
        r = subprocess.run(
            ["mongosh", "--quiet", "--eval", js],
            capture_output=True,
            text=True,
            timeout=15,
        )
        if r.returncode != 0:
            return False, (r.stderr or r.stdout or "Failed").strip()[:200]
        return True, "Database dropped"
    except FileNotFoundError:
        return False, "MongoDB (mongosh) not found"


def backup_postgresql_database(db_name: str, backup_dir: str) -> tuple[bool, str, str | None]:
    """Create PostgreSQL backup via pg_dump. Returns (success, message, filename)."""
    if not all(c.isalnum() or c == "_" for c in db_name):
        return False, "Invalid database name", None
    os.makedirs(backup_dir, exist_ok=True)
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    filename = f"{db_name}_{ts}.sql.gz"
    dest = os.path.join(backup_dir, filename)
    try:
        r = subprocess.run(
            f'sudo -u postgres pg_dump {db_name} | gzip > "{dest}"',
            shell=True,
            timeout=300,
            capture_output=True,
            text=True,
        )
        if r.returncode != 0 or not os.path.isfile(dest):
            return False, (r.stderr or r.stdout or "Backup failed").strip()[:200], None
        return True, "Backup created", filename
    except subprocess.TimeoutExpired:
        return False, "Timed out", None
    except Exception as e:
        return False, str(e), None


def restore_postgresql_database(db_name: str, backup_path: str) -> tuple[bool, str]:
    """Restore PostgreSQL database from .sql.gz backup."""
    if not all(c.isalnum() or c == "_" for c in db_name):
        return False, "Invalid database name"
    if not os.path.isfile(backup_path):
        return False, "Backup file not found"
    try:
        r = subprocess.run(
            f'gunzip -c "{backup_path}" | sudo -u postgres psql -d {db_name}',
            shell=True,
            timeout=300,
            capture_output=True,
            text=True,
        )
        if r.returncode != 0:
            return False, (r.stderr or r.stdout or "Restore failed").strip()[:200]
        return True, "Restored"
    except subprocess.TimeoutExpired:
        return False, "Timed out"
    except Exception as e:
        return False, str(e)


def backup_mongodb_database(db_name: str, backup_dir: str) -> tuple[bool, str, str | None]:
    """Create MongoDB backup via mongodump. Returns (success, message, filename)."""
    if not all(c.isalnum() or c == "_" for c in db_name):
        return False, "Invalid database name", None
    os.makedirs(backup_dir, exist_ok=True)
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    out_dir = os.path.join(backup_dir, f"{db_name}_{ts}")
    try:
        r = subprocess.run(
            ["mongodump", "--db", db_name, "--out", out_dir, "--gzip"],
            capture_output=True,
            text=True,
            timeout=300,
        )
        if r.returncode != 0:
            return False, (r.stderr or r.stdout or "Backup failed").strip()[:200], None
        # Create tarball of the dump
        archive = out_dir + ".tar.gz"
        r2 = subprocess.run(
            f'tar -czf "{archive}" -C "{backup_dir}" "{os.path.basename(out_dir)}"',
            shell=True,
            capture_output=True,
            text=True,
            timeout=120,
        )
        if os.path.isdir(out_dir):
            shutil.rmtree(out_dir, ignore_errors=True)
        if r2.returncode != 0 or not os.path.isfile(archive):
            return False, "Archive failed", None
        return True, "Backup created", os.path.basename(archive)
    except subprocess.TimeoutExpired:
        return False, "Timed out", None
    except Exception as e:
        return False, str(e), None


def restore_mongodb_database(db_name: str, backup_path: str) -> tuple[bool, str]:
    """Restore MongoDB database from .tar.gz mongodump backup."""
    if not all(c.isalnum() or c == "_" for c in db_name):
        return False, "Invalid database name"
    if not os.path.isfile(backup_path):
        return False, "Backup file not found"
    import tempfile
    tmp = tempfile.mkdtemp()
    try:
        r = subprocess.run(
            ["tar", "-xzf", backup_path, "-C", tmp],
            capture_output=True,
            text=True,
            timeout=60,
        )
        if r.returncode != 0:
            return False, (r.stderr or r.stdout or "Extract failed").strip()[:200]
        # Find the extracted dir (mongodump creates db_name/ subdir)
        extracted = os.path.join(tmp, os.listdir(tmp)[0]) if os.listdir(tmp) else None
        if not extracted or not os.path.isdir(extracted):
            return False, "Invalid backup format"
        r2 = subprocess.run(
            ["mongorestore", "--db", db_name, "--gzip", "--drop", extracted],
            capture_output=True,
            text=True,
            timeout=300,
        )
        if r2.returncode != 0:
            return False, (r2.stderr or r2.stdout or "Restore failed").strip()[:200]
        return True, "Restored"
    except Exception as e:
        return False, str(e)
    finally:
        shutil.rmtree(tmp, ignore_errors=True)


def change_mongodb_password(username: str, db_name: str, new_password: str) -> tuple[bool, str]:
    """Change MongoDB user password."""
    if not all(c.isalnum() or c == "_" for c in username):
        return False, "Invalid username"
    pw_esc = new_password.replace("\\", "\\\\").replace("'", "\\'")
    js = f"db = db.getSiblingDB('{db_name}'); db.changeUserPassword('{username}', '{pw_esc}');"
    try:
        r = subprocess.run(
            ["mongosh", "--quiet", "--eval", js],
            capture_output=True,
            text=True,
            timeout=15,
        )
        if r.returncode != 0:
            return False, (r.stderr or r.stdout or "Failed").strip()[:200]
        return True, "Password updated"
    except FileNotFoundError:
        return False, "MongoDB (mongosh) not found"
