import os
import shutil
import zipfile
from pathlib import Path
from typing import Optional
from urllib.parse import urlparse
from uuid import uuid4

from dotenv import load_dotenv
from fastapi import FastAPI, File, Form, Request, UploadFile
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates

from deploy import deploy, slugify

BASE_DIR = Path(__file__).resolve().parent

load_dotenv(BASE_DIR / ".env")

app = FastAPI(title="Deploy Interno cPanel")

templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))

UPLOAD_DIR = BASE_DIR / "uploads"
WORKDIR = BASE_DIR / "workdir"

UPLOAD_DIR.mkdir(exist_ok=True)
WORKDIR.mkdir(exist_ok=True)

ROOT_DOMAIN = os.getenv("ROOT_DOMAIN", "grupoddm.ia.br")

ALLOWED_EXTENSIONS = [".zip", ".html", ".htm"]


def extract_slug_from_project_url(project_url: str) -> str:
    project_url = project_url.strip()

    if not project_url:
        raise ValueError("Informe a URL do projeto existente.")

    if not project_url.startswith(("http://", "https://")):
        project_url = "https://" + project_url

    parsed = urlparse(project_url)
    hostname = parsed.hostname

    if not hostname:
        raise ValueError("URL inválida.")

    hostname = hostname.lower()
    suffix = "." + ROOT_DOMAIN.lower()

    if not hostname.endswith(suffix):
        raise ValueError(f"A URL precisa pertencer ao domínio {ROOT_DOMAIN}.")

    slug = hostname[: -len(suffix)]

    if not slug:
        raise ValueError("A URL informada não parece ser um projeto publicado.")

    if "." in slug:
        raise ValueError("Use a URL principal do projeto, sem sub-subdomínios.")

    return slugify(slug)


def safe_extract_zip(zip_path: Path, extract_to: Path) -> None:
    extract_to = extract_to.resolve()

    try:
        with zipfile.ZipFile(zip_path, "r") as zip_ref:
            for member in zip_ref.infolist():
                member_path = (extract_to / member.filename).resolve()

                if not str(member_path).startswith(str(extract_to)):
                    raise ValueError(
                        "ZIP inválido: tentativa de escrita fora da pasta do projeto."
                    )

            zip_ref.extractall(extract_to)

    except zipfile.BadZipFile:
        raise ValueError("O arquivo enviado não parece ser um ZIP válido.")


def normalize_extracted_folder(extract_to: Path) -> Path:
    items = [
        item
        for item in extract_to.iterdir()
        if item.name not in ["__MACOSX"]
    ]

    if len(items) == 1 and items[0].is_dir():
        return items[0]

    return extract_to


def get_file_extension(filename: str) -> str:
    return Path(filename).suffix.lower()


def prepare_uploaded_project(file: UploadFile, slug: str) -> Path:
    if not file.filename:
        raise ValueError("Envie um arquivo .zip, .html ou .htm.")

    original_filename = Path(file.filename).name
    extension = get_file_extension(original_filename)

    if extension not in ALLOWED_EXTENSIONS:
        raise ValueError("Envie um arquivo .zip, .html ou .htm.")

    job_id = uuid4().hex
    extract_to = WORKDIR / f"{job_id}-{slug}"
    extract_to.mkdir(parents=True, exist_ok=True)

    if extension == ".zip":
        zip_path = UPLOAD_DIR / f"{job_id}-{slug}.zip"

        with zip_path.open("wb") as buffer:
            shutil.copyfileobj(file.file, buffer)

        safe_extract_zip(zip_path, extract_to)

        return normalize_extracted_folder(extract_to)

    html_upload_path = UPLOAD_DIR / f"{job_id}-{slug}{extension}"

    with html_upload_path.open("wb") as buffer:
        shutil.copyfileobj(file.file, buffer)

    destination_index = extract_to / "index.html"
    shutil.copyfile(html_upload_path, destination_index)

    return extract_to


def base_context(
    result_url: Optional[str] = None,
    error: Optional[str] = None,
    project_name: str = "",
    existing_url: str = "",
    mode: str = "create",
) -> dict:
    return {
        "result_url": result_url,
        "error": error,
        "project_name": project_name,
        "existing_url": existing_url,
        "mode": mode,
    }


@app.get("/", response_class=HTMLResponse)
def home(request: Request):
    return templates.TemplateResponse(
        request,
        "index.html",
        base_context(),
    )


@app.post("/deploy", response_class=HTMLResponse)
async def deploy_project(
    request: Request,
    mode: str = Form("create"),
    project_name: str = Form(""),
    existing_url: str = Form(""),
    file: UploadFile = File(...),
):
    try:
        if mode not in ["create", "update"]:
            raise ValueError("Modo de deploy inválido.")

        if mode == "create":
            if not project_name.strip():
                raise ValueError("Informe o nome do projeto.")

            deploy_name = project_name.strip()
            slug = slugify(deploy_name)

        else:
            slug = extract_slug_from_project_url(existing_url)
            deploy_name = slug

        project_folder = prepare_uploaded_project(file=file, slug=slug)

        result_url = deploy(
            project_name=deploy_name,
            source=project_folder,
        )

        return templates.TemplateResponse(
            request,
            "index.html",
            base_context(
                result_url=result_url,
                error=None,
                project_name=project_name,
                existing_url=existing_url,
                mode=mode,
            ),
        )

    except Exception as exc:
        return templates.TemplateResponse(
            request,
            "index.html",
            base_context(
                result_url=None,
                error=str(exc),
                project_name=project_name,
                existing_url=existing_url,
                mode=mode,
            ),
            status_code=400,
        )