import argparse
import mimetypes
import os
import re
import unicodedata
from pathlib import Path
from typing import Optional

import requests
import urllib3
from dotenv import load_dotenv

BASE_DIR = Path(__file__).resolve().parent

load_dotenv(BASE_DIR / ".env")

HOST = os.getenv("CPANEL_HOST")
USER = os.getenv("CPANEL_USER")
TOKEN = os.getenv("CPANEL_TOKEN")

ROOT_DOMAIN = os.getenv("ROOT_DOMAIN", "grupoddm.ia.br")
APP_ROOT_ABSOLUTE = f"/home/{USER}/apps"

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


def slugify(value: str) -> str:
    value = unicodedata.normalize("NFKD", value)
    value = value.encode("ascii", "ignore").decode("ascii")
    value = value.lower()
    value = re.sub(r"[^a-z0-9]+", "-", value)
    value = value.strip("-")
    value = re.sub(r"-+", "-", value)

    if not value:
        raise ValueError("Nome do projeto inválido.")

    return value


def headers() -> dict:
    return {
        "Authorization": f"cpanel {USER}:{TOKEN}"
    }


def validate_env() -> None:
    missing = []

    if not HOST:
        missing.append("CPANEL_HOST")

    if not USER:
        missing.append("CPANEL_USER")

    if not TOKEN:
        missing.append("CPANEL_TOKEN")

    if missing:
        raise RuntimeError(
            "Variáveis ausentes no .env: " + ", ".join(missing)
        )


def api2(module: str, function: str, params: dict) -> dict:
    validate_env()

    url = f"{HOST}/json-api/cpanel"

    payload = {
        "cpanel_jsonapi_user": USER,
        "cpanel_jsonapi_apiversion": "2",
        "cpanel_jsonapi_module": module,
        "cpanel_jsonapi_func": function,
        **params,
    }

    response = requests.get(
        url,
        headers=headers(),
        params=payload,
        verify=False,
        timeout=60,
    )

    try:
        data = response.json()
    except ValueError:
        raise RuntimeError(response.text)

    return data


def uapi(module: str, function: str, params: Optional[dict] = None) -> dict:
    validate_env()

    url = f"{HOST}/execute/{module}/{function}"

    response = requests.get(
        url,
        headers=headers(),
        params=params or {},
        verify=False,
        timeout=60,
    )

    try:
        data = response.json()
    except ValueError:
        raise RuntimeError(response.text)

    return data


def ensure_project_folder(slug: str) -> None:
    data = api2(
        module="Fileman",
        function="mkdir",
        params={
            "path": APP_ROOT_ABSOLUTE,
            "name": slug,
            "permissions": "0755",
        },
    )

    event = data.get("cpanelresult", {}).get("event", {})
    result = event.get("result")
    reason = event.get("reason", "")

    if result == 1:
        print(f"PASTA CRIADA: {APP_ROOT_ABSOLUTE}/{slug}")
        return

    reason_lower = reason.lower()

    if (
        "exists" in reason_lower
        or "already" in reason_lower
        or "existe" in reason_lower
    ):
        print(f"PASTA JÁ EXISTE: {APP_ROOT_ABSOLUTE}/{slug}")
        return

    raise RuntimeError(f"Erro ao criar pasta: {data}")


def ensure_remote_folder(relative_path: str) -> None:
    """
    Cria subpastas dentro de /home/grpia/apps quando necessário.

    Exemplo:
    relative_path = "meu-projeto/assets/css"
    """
    full_parts = relative_path.replace("\\", "/").strip("/").split("/")

    current = APP_ROOT_ABSOLUTE

    for part in full_parts:
        if not part or part == ".":
            continue

        data = api2(
            module="Fileman",
            function="mkdir",
            params={
                "path": current,
                "name": part,
                "permissions": "0755",
            },
        )

        event = data.get("cpanelresult", {}).get("event", {})
        result = event.get("result")
        reason = event.get("reason", "")

        if result != 1:
            reason_lower = reason.lower()

            if (
                "exists" not in reason_lower
                and "already" not in reason_lower
                and "existe" not in reason_lower
            ):
                raise RuntimeError(
                    f"Erro ao criar subpasta {current}/{part}: {data}"
                )

        current = f"{current}/{part}"


def upload_file(local_file: Path, remote_dir: str) -> None:
    validate_env()

    content_type = (
        mimetypes.guess_type(local_file.name)[0]
        or "application/octet-stream"
    )

    url = f"{HOST}/execute/Fileman/upload_files"

    with local_file.open("rb") as file:
        files = {
            "file-1": (local_file.name, file, content_type),
        }

        data = {
            "dir": remote_dir,
            "overwrite": "1",
        }

        response = requests.post(
            url,
            headers=headers(),
            data=data,
            files=files,
            verify=False,
            timeout=120,
        )

    try:
        result = response.json()
    except ValueError:
        raise RuntimeError(response.text)

    if response.status_code == 200 and result.get("status") == 1:
        print(f"UPLOAD: {local_file} -> {remote_dir}/{local_file.name}")
        return

    raise RuntimeError(f"Erro no upload de {local_file}: {result}")


def upload_project(source: Path, slug: str) -> None:
    if not source.exists():
        raise FileNotFoundError(f"Pasta local não encontrada: {source}")

    if not source.is_dir():
        raise ValueError(
            "Por enquanto o deploy espera uma pasta, não um arquivo zip."
        )

    files = [path for path in source.rglob("*") if path.is_file()]

    if not files:
        raise ValueError("A pasta do projeto está vazia.")

    for file_path in files:
        relative_parent = file_path.parent.relative_to(source)
        remote_relative_dir = Path(slug) / relative_parent
        remote_relative_dir_str = str(remote_relative_dir).replace("\\", "/")
        remote_absolute_dir = f"{APP_ROOT_ABSOLUTE}/{remote_relative_dir_str}"

        ensure_remote_folder(remote_relative_dir_str)
        upload_file(file_path, remote_absolute_dir)


def create_subdomain(slug: str) -> str:
    document_root = f"apps/{slug}"

    data = uapi(
        module="SubDomain",
        function="addsubdomain",
        params={
            "domain": slug,
            "rootdomain": ROOT_DOMAIN,
            "dir": document_root,
            "disallowdot": "1",
        },
    )

    url = f"https://{slug}.{ROOT_DOMAIN}"

    if data.get("status") == 1:
        print(f"SUBDOMÍNIO CRIADO: {url}")
        return url

    errors = data.get("errors") or []
    error_text = " ".join(str(error) for error in errors).lower()
    data_text = str(data).lower()

    if (
        "already exists" in error_text
        or "already exists" in data_text
        or "existe" in data_text
    ):
        print(f"SUBDOMÍNIO JÁ EXISTE: {url}")
        return url

    raise RuntimeError(f"Erro ao criar subdomínio: {data}")


def deploy(project_name: str, source: Path) -> str:
    slug = slugify(project_name)

    print()
    print("INICIANDO DEPLOY")
    print(f"Projeto: {project_name}")
    print(f"Slug: {slug}")
    print(f"Origem local: {source}")
    print(f"Destino remoto: {APP_ROOT_ABSOLUTE}/{slug}")
    print()

    ensure_project_folder(slug)
    upload_project(source, slug)
    url = create_subdomain(slug)

    print()
    print("DEPLOY CONCLUÍDO")
    print(url)

    return url


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--name", required=True, help="Nome do projeto")
    parser.add_argument("--source", required=True, help="Pasta local do projeto")

    args = parser.parse_args()

    deploy(
        project_name=args.name,
        source=Path(args.source),
    )


if __name__ == "__main__":
    main()