#!/usr/bin/env python3
"""Manage cPanel MySQL databases and users through the cPanel UAPI.

Credentials can be set as environment variables:
    CPANEL_HOST         e.g. business122.web-hosting.com
    CPANEL_USER         cPanel username
    CPANEL_API_TOKEN    cPanel API token (create one in cPanel → Security → Manage API Tokens)

Or passed as command-line flags.

Examples:
    cpanel_mysql.py list-dbs
    cpanel_mysql.py create-db ssfx_v3
    cpanel_mysql.py create-user ssfxuser 'Str0ngP@ss'
    cpanel_mysql.py grant --user ssfxuser --db cpaneluser_ssfx_v3 --privileges ALL
    cpanel_mysql.py list-privs --user ssfxuser --db cpaneluser_ssfx_v3
    cpanel_mysql.py revoke --user ssfxuser --db cpaneluser_ssfx_v3
    cpanel_mysql.py delete-db cpaneluser_ssfx_v3 --force
"""

from __future__ import annotations

import argparse
import json
import os
import secrets
import string
import sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any

from dotenv import load_dotenv


DEFAULT_PRIVILEGES = "ALL"


def _env(name: str) -> str:
    return os.getenv(name, "").strip()


def _random_password(length: int = 24) -> str:
    alphabet = string.ascii_letters + string.digits + "_!@#$%^&*"
    return "".join(secrets.choice(alphabet) for _ in range(length))


class CPanelMySQL:
    def __init__(self, host: str, user: str, token: str, port: int = 2083):
        self.host = host.rstrip("/")
        self.user = user
        self.token = token
        self.port = port
        self.base_url = f"https://{self.host}:{self.port}/execute/Mysql"

    def _call(self, function: str, params: dict[str, Any]) -> dict[str, Any]:
        query = urllib.parse.urlencode(params, quote_via=urllib.parse.quote)
        url = f"{self.base_url}/{function}"
        if query:
            url += f"?{query}"

        req = urllib.request.Request(
            url,
            headers={
                "Authorization": f"cpanel {self.user}:{self.token}",
                "Accept": "application/json",
            },
        )

        try:
            with urllib.request.urlopen(req, timeout=60) as resp:
                data = json.loads(resp.read().decode("utf-8"))
        except urllib.error.HTTPError as exc:
            body = exc.read().decode("utf-8", errors="ignore")
            try:
                data = json.loads(body)
            except Exception:
                data = {"status": 0, "errors": [body or str(exc)]}
        except urllib.error.URLError as exc:
            data = {"status": 0, "errors": [str(exc.reason)]}
        except Exception as exc:
            data = {"status": 0, "errors": [str(exc)]}

        return data

    @staticmethod
    def _check(data: dict[str, Any]) -> dict[str, Any]:
        if not data.get("status"):
            errors = data.get("errors") or data.get("messages") or ["Unknown cPanel error"]
            for err in errors:
                print(f"Error: {err}", file=sys.stderr)
            sys.exit(1)
        return data

    def list_databases(self) -> list[dict[str, Any]]:
        data = self._check(self._call("list_databases", {}))
        return data.get("data", [])

    def create_database(self, name: str) -> dict[str, Any]:
        return self._check(self._call("create_database", {"name": name}))

    def delete_database(self, name: str) -> dict[str, Any]:
        return self._check(self._call("delete_database", {"name": name}))

    def list_users(self) -> list[dict[str, Any]]:
        data = self._check(self._call("list_users", {}))
        return data.get("data", [])

    def create_user(self, name: str, password: str) -> dict[str, Any]:
        return self._check(
            self._call("create_user", {"name": name, "password": password})
        )

    def delete_user(self, name: str) -> dict[str, Any]:
        return self._check(self._call("delete_user", {"name": name}))

    def set_password(self, name: str, password: str) -> dict[str, Any]:
        return self._check(
            self._call("set_password", {"user": name, "password": password})
        )

    def get_privileges(self, user: str, database: str) -> list[str]:
        data = self._check(
            self._call("get_privileges_on_database", {"user": user, "database": database})
        )
        return data.get("data", {}).get("privileges", []) if isinstance(data.get("data"), dict) else []

    def set_privileges(self, user: str, database: str, privileges: str) -> dict[str, Any]:
        return self._check(
            self._call(
                "set_privileges_on_database",
                {"user": user, "database": database, "privileges": privileges},
            )
        )

    def revoke_access(self, user: str, database: str) -> dict[str, Any]:
        return self._check(
            self._call("revoke_access_to_database", {"user": user, "database": database})
        )


def _pick_format(fmt: str, data: Any) -> str:
    if fmt == "json":
        return json.dumps(data, indent=2, default=str)

    if isinstance(data, list):
        lines: list[str] = []
        for item in data:
            if isinstance(item, dict):
                lines.append("  " + "  ".join(f"{k}={v}" for k, v in item.items()))
            else:
                lines.append(f"  {item}")
        return "\n".join(lines) if lines else "  (none)"

    return json.dumps(data, indent=2, default=str)


def _get_client(args: argparse.Namespace) -> CPanelMySQL:
    host = args.host or _env("CPANEL_HOST")
    user = args.user or _env("CPANEL_USER")
    token = args.token or _env("CPANEL_API_TOKEN")

    missing = [k for k, v in {
        "CPANEL_HOST": host,
        "CPANEL_USER": user,
        "CPANEL_API_TOKEN": token,
    }.items() if not v]
    if missing:
        print(
            f"Missing credentials: {', '.join(missing)}. "
            "Set env vars or pass --host/--user/--token.",
            file=sys.stderr,
        )
        sys.exit(1)

    return CPanelMySQL(host, user, token, port=args.port)


def cmd_list_dbs(args: argparse.Namespace) -> int:
    api = _get_client(args)
    dbs = api.list_databases()
    print(_pick_format(args.format, dbs))
    return 0


def cmd_create_db(args: argparse.Namespace) -> int:
    api = _get_client(args)
    result = api.create_database(args.name)
    print(f"Created database: {args.name}")
    if args.format == "json":
        print(json.dumps(result, indent=2, default=str))
    return 0


def cmd_delete_db(args: argparse.Namespace) -> int:
    api = _get_client(args)
    if not args.force:
        confirm = input(f"Delete database '{args.name}'? [y/N] ")
        if confirm.lower() not in ("y", "yes"):
            print("Aborted")
            return 1
    result = api.delete_database(args.name)
    print(f"Deleted database: {args.name}")
    if args.format == "json":
        print(json.dumps(result, indent=2, default=str))
    return 0


def cmd_list_users(args: argparse.Namespace) -> int:
    api = _get_client(args)
    users = api.list_users()
    print(_pick_format(args.format, users))
    return 0


def cmd_create_user(args: argparse.Namespace) -> int:
    api = _get_client(args)
    password = args.password
    if not password:
        password = _random_password()
        mode = "generated"
    else:
        mode = "provided"
    result = api.create_user(args.name, password)
    print(f"Created user: {args.name}")
    if mode == "generated":
        print(f"Password: {password}")
    if args.format == "json":
        print(json.dumps(result, indent=2, default=str))
    return 0


def cmd_set_password(args: argparse.Namespace) -> int:
    api = _get_client(args)
    api.set_password(args.name, args.password)
    print(f"Password reset for user: {args.name}")
    return 0


def cmd_delete_user(args: argparse.Namespace) -> int:
    api = _get_client(args)
    if not args.force:
        confirm = input(f"Delete user '{args.name}'? [y/N] ")
        if confirm.lower() not in ("y", "yes"):
            print("Aborted")
            return 1
    result = api.delete_user(args.name)
    print(f"Deleted user: {args.name}")
    if args.format == "json":
        print(json.dumps(result, indent=2, default=str))
    return 0


def cmd_list_privs(args: argparse.Namespace) -> int:
    api = _get_client(args)
    privs = api.get_privileges(args.user, args.database)
    print(_pick_format(args.format, privs))
    return 0


def cmd_grant(args: argparse.Namespace) -> int:
    api = _get_client(args)
    result = api.set_privileges(args.user, args.database, args.privileges)
    print(f"Granted '{args.privileges}' on {args.database} to {args.user}")
    if args.format == "json":
        print(json.dumps(result, indent=2, default=str))
    return 0


def cmd_revoke(args: argparse.Namespace) -> int:
    api = _get_client(args)
    result = api.revoke_access(args.user, args.database)
    print(f"Revoked {args.user} access from {args.database}")
    if args.format == "json":
        print(json.dumps(result, indent=2, default=str))
    return 0


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(
        description="Manage cPanel MySQL databases and users via UAPI",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Credentials (in order of precedence):
  1. Command-line flags: --host, --user, --token
  2. Environment variables: CPANEL_HOST, CPANEL_USER, CPANEL_API_TOKEN
  3. .env file: CPANEL_HOST, CPANEL_USER, CPANEL_API_TOKEN

Create an API token in cPanel → Security → Manage API Tokens.
        """,
    )
    parser.add_argument("--host", help="cPanel host (or CPANEL_HOST)")
    parser.add_argument("--user", help="cPanel username (or CPANEL_USER)")
    parser.add_argument("--token", help="cPanel API token (or CPANEL_API_TOKEN)")
    parser.add_argument("--port", type=int, default=2083, help="cPanel port (default 2083)")
    parser.add_argument(
        "--env", default=str(Path(__file__).resolve().parent.parent / ".env"),
        help="Path to .env file containing CPANEL_HOST, CPANEL_USER, CPANEL_API_TOKEN"
    )
    parser.add_argument(
        "--format", choices=["text", "json"], default="text",
        help="Output format"
    )

    args, _ = parser.parse_known_args(argv)
    if args.env and Path(args.env).exists():
        load_dotenv(args.env, override=False)

    sub = parser.add_subparsers(dest="command", required=True)

    sub.add_parser("list-dbs", help="List databases").set_defaults(func=cmd_list_dbs)

    p_create_db = sub.add_parser("create-db", help="Create a database")
    p_create_db.add_argument("name", help="Database name")
    p_create_db.set_defaults(func=cmd_create_db)

    p_delete_db = sub.add_parser("delete-db", help="Delete a database")
    p_delete_db.add_argument("name", help="Database name")
    p_delete_db.add_argument("--force", action="store_true", help="Skip confirmation")
    p_delete_db.set_defaults(func=cmd_delete_db)

    sub.add_parser("list-users", help="List database users").set_defaults(func=cmd_list_users)

    p_create_user = sub.add_parser("create-user", help="Create a database user")
    p_create_user.add_argument("name", help="Username")
    p_create_user.add_argument("password", nargs="?", help="Password (omit to generate one)")
    p_create_user.set_defaults(func=cmd_create_user)

    p_delete_user = sub.add_parser("delete-user", help="Delete a database user")
    p_delete_user.add_argument("name", help="Username")
    p_delete_user.add_argument("--force", action="store_true", help="Skip confirmation")
    p_delete_user.set_defaults(func=cmd_delete_user)

    p_set_pw = sub.add_parser("set-password", help="Reset a database user's password")
    p_set_pw.add_argument("name", help="Username")
    p_set_pw.add_argument("password", help="New password")
    p_set_pw.set_defaults(func=cmd_set_password)

    p_list_privs = sub.add_parser("list-privs", help="List user privileges on a database")
    p_list_privs.add_argument("--user", required=True, help="Database username")
    p_list_privs.add_argument("--db", required=True, help="Database name")
    p_list_privs.set_defaults(func=cmd_list_privs)

    p_grant = sub.add_parser("grant", help="Set user privileges on a database")
    p_grant.add_argument("--user", required=True, help="Database username")
    p_grant.add_argument("--db", required=True, help="Database name")
    p_grant.add_argument(
        "--privileges", default=DEFAULT_PRIVILEGES,
        help="Comma-separated privileges or ALL (default: ALL)"
    )
    p_grant.set_defaults(func=cmd_grant)

    p_revoke = sub.add_parser("revoke", help="Revoke user access to a database")
    p_revoke.add_argument("--user", required=True, help="Database username")
    p_revoke.add_argument("--db", required=True, help="Database name")
    p_revoke.set_defaults(func=cmd_revoke)

    args = parser.parse_args(argv)
    return args.func(args)


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