from __future__ import annotations

import re
from pathlib import Path
from typing import List, Optional
from urllib.parse import quote

from fastapi import HTTPException
from fastapi.responses import FileResponse
from starlette.background import BackgroundTask

from server.data.definitions import ApplicationData
from server.repositories.FileRepository import FileRepository
from server.utils.files import buildArchive, deleteFile


class FileService:
    """
    Service for bundling, listing, and deleting files.
    """

    def __init__(self, repo: FileRepository, appData: ApplicationData, outputDir: Optional[str | Path] = None) -> None:
        """
        Initialize file bundling service.

        :param repo: Repository for file indexing and resolution
        :param meta: Metadata provider for archive naming
        :param outputDir: Optional directory for storing created ZIPs
        """
        self._repo = repo
        self._app = appData
        self._outputDir = Path(outputDir) if outputDir else None

    async def listIds(self) -> List[dict]:
        """
        List all available file IDs.

        :return: [{"id": <file_name>}]
        """
        idx = await self._repo.index()
        return [{"id": n} for n in sorted(idx)]

    async def zipOne(self, fileId: str) -> FileResponse:
        """
        Create ZIP containing a single file.

        :param fileId: Name of the file to include
        :return: FileResponse for the ZIP archive
        """
        self._validateId(fileId)
        fullPath = await self._repo.resolve(fileId)
        if not fullPath:
            raise _notFound()

        zipLabel = re.sub(r"[^a-zA-Z0-9 _.,@\-]", "", fileId)
        kwargs = {}
        if self._outputDir is not None:
            self._outputDir.mkdir(parents=True, exist_ok=True)
            kwargs["outputDir"] = str(self._outputDir)

        zipPathStr, zipName = await buildArchive(
            serial=self._app.serial,
            ecpVersion=self._app.ecpVersion,
            fileName=zipLabel,
            filePaths=[str(fullPath.parent)],
            files=[fileId],
            **kwargs,
        )
        return _zipResponse(Path(zipPathStr), zipName)

    async def zipMany(self, label: str = "files") -> FileResponse:
        """
        Create ZIP containing all indexed files.

        :param label: Label for the archive name
        :return: FileResponse for the ZIP archive
        """
        idx = await self._repo.index()
        include = sorted(idx)
        if not include:
            raise _notFound()

        roots = sorted({str(idx[name].parent) for name in include})
        zipLabel = re.sub(r"[^a-zA-Z0-9 _.,@\-]", "", label)
        kwargs = {}
        if self._outputDir is not None:
            self._outputDir.mkdir(parents=True, exist_ok=True)
            kwargs["outputDir"] = str(self._outputDir)

        zipPathStr, zipName = await buildArchive(
            serial=self._app.serial,
            ecpVersion=self._app.ecpVersion,
            fileName=zipLabel,
            filePaths=roots,
            files=include,
            **kwargs,
        )
        return _zipResponse(Path(zipPathStr), zipName)

    async def deleteOne(self, fileId: str) -> dict:
        """
        Delete a file by name.

        :param fileId: Name of the file to delete
        :return: {"deleted": <file_name>, "path": <parent_dir>}
        """
        self._validateId(fileId)
        fullPath = await self._repo.resolve(fileId)
        if not fullPath:
            raise _notFound()

        ok = await deleteFile(path=str(fullPath.parent), deleteFileName=fileId)
        if not ok:
            raise _notFound()
        return {"deleted": fileId, "path": str(fullPath.parent)}

    def _validateId(self, s: str) -> None:
        """
        Ensure file ID is a valid name without path separators.

        :param s: File ID to validate
        """
        p = Path(s)
        if p.name != s or any(sep in s for sep in ("/", "\\")):
            raise HTTPException(status_code=400, detail="Invalid file id")


def _zipResponse(zipPath: Path, zipNameWoExt: str) -> FileResponse:
    """
    Create a FileResponse for a ZIP archive with proper headers.

    :param zipPath: Path to the ZIP file
    :param zipNameWoExt: Archive name without extension
    :return: FileResponse object
    """
    finalName = zipNameWoExt if zipNameWoExt.lower().endswith(".zip") else f"{zipNameWoExt}.zip"
    asciiFallback = (finalName.encode("ascii", "ignore").decode() or "download.zip").replace(" ", "_")
    contentDisp = f"attachment; filename=\"{asciiFallback}\"; filename*=UTF-8''{quote(finalName)}"

    resp = FileResponse(
        zipPath,
        media_type="application/zip",
        headers={"Cache-Control": "no-store"},
        background=BackgroundTask(_safeUnlink, zipPath),
    )
    resp.headers["Content-Disposition"] = contentDisp
    return resp


def _notFound() -> HTTPException:
    """
    Create HTTP 404 error for missing file.

    :return: HTTPException with 404 status
    """
    return HTTPException(status_code=404, detail={"type": "FILE_NOT_FOUND", "message": "File not found"})


def _safeUnlink(path: str | Path) -> None:
    """
    Remove a file, ignoring errors.

    :param path: File path to remove
    """
    try:
        Path(path).unlink(missing_ok=True)
    except Exception:
        pass
