from __future__ import annotations

import json
import mimetypes
import subprocess
from enum import Enum
from pathlib import Path
from typing import Generator

from fastapi import APIRouter, Header, HTTPException, Query, Request
from fastapi.responses import Response, StreamingResponse
from pydantic import BaseModel

from remotivelabs.cli.search import SearchAPI
from remotivelabs.cli.studio.api.path_utils import PathError, join_under, to_relative_path
from remotivelabs.cli.topology.cli.topology_cli import run_topology

router = APIRouter(prefix="/api/studio/v1/files", tags=["files"])


def _get_search_index(request: Request) -> SearchAPI:
    return request.app.state.search_index  # type: ignore[no-any-return]


class FileType(str, Enum):
    FOLDER = "FILE_TYPE_FOLDER"
    INSTANCE = "FILE_TYPE_INSTANCE"
    PLATFORM = "FILE_TYPE_PLATFORM"
    RECORDING_SESSION = "FILE_TYPE_RECORDING_SESSION"
    RECORDING_MAPPING = "FILE_TYPE_RECORDING_MAPPING"
    RECORDING = "FILE_TYPE_RECORDING"
    VIDEO = "FILE_TYPE_VIDEO"
    AUDIO = "FILE_TYPE_AUDIO"
    IMAGE = "FILE_TYPE_IMAGE"
    TEXT = "FILE_TYPE_TEXT"
    UNKNOWN = "FILE_TYPE_UNKNOWN"
    SIGNAL_DATABASE = "FILE_TYPE_SIGNAL_DATABASE"
    DASHBOARD = "FILE_TYPE_DASHBOARD"


class ListFilesFile(BaseModel):
    path: str
    type: FileType
    size: int | None
    modified_time: int
    created_time: int


class ListFilesResponse(BaseModel):
    files: list[ListFilesFile]


def describe_path(root: Path, p: Path) -> ListFilesFile:
    path = "/" + str(p.relative_to(root))
    stat = p.stat()
    file_type = get_file_type(p)
    if file_type == FileType.FOLDER:
        return ListFilesFile(
            path=path,
            type=file_type,
            size=None,
            modified_time=int(stat.st_mtime),
            created_time=int(stat.st_ctime),
        )
    return ListFilesFile(
        path=path,
        type=file_type,
        size=stat.st_size,
        modified_time=int(stat.st_mtime),
        created_time=int(stat.st_ctime),
    )


@router.get("/platform")
def platform_file(
    request: Request,
    # background_tasks: BackgroundTasks,
    path: str = Query(..., description="File path to show platform"),
) -> Response:
    return run_cmd_and_get_yaml(["show", "platform"], request, path)


@router.get("/instance")
def instance_file(
    request: Request,
    path: str = Query(..., description="File path to show instance"),
) -> Response:
    return run_cmd_and_get_yaml(["show", "instance", "--full-platform"], request, path)


@router.get("/remotivedb")
def convert_to_remotivedb(
    request: Request,
    path: str = Query(..., description="File path to convert to RemotiveDB"),
) -> Response:
    yaml_db = run_cmd_and_get_yaml(["convert", "--file"], request, path)
    workspace = request.app.state.topology.workspace
    workspace_path = str(join_under(workspace, Path(path)).resolve())
    rel_path = to_relative_path(workspace, Path(path))
    raw = bytes(yaml_db.body).decode("utf-8")
    search_index = _get_search_index(request)
    search_index.index_yaml_data(rel_path, workspace_path, raw)
    return Response(content=bytes(yaml_db.body), media_type="application/yaml")


@router.get("/download")
def download_file(
    request: Request,
    path: str = Query(..., description="File path to download"),
    range: str | None = Header(None),
) -> StreamingResponse:
    file_path = join_under(request.app.state.topology.workspace, Path(path))
    if not file_path.exists():
        raise HTTPException(status_code=404, detail=PathError(message="File not found", path=str(file_path)).model_dump())
    if not file_path.is_file():
        raise HTTPException(status_code=400, detail=PathError(message="Not a file", path=str(file_path)).model_dump())

    file_size = file_path.stat().st_size
    content_type, _ = mimetypes.guess_type(str(file_path))
    if content_type is None:
        content_type = "application/octet-stream"

    start = 0
    end = file_size - 1

    if range:
        range_match = range.replace("bytes=", "").split("-")
        start = int(range_match[0]) if range_match[0] else 0
        end = int(range_match[1]) if range_match[1] else file_size - 1

    chunk_size = 1024 * 1024  # 1MB chunks

    def iter_file() -> Generator[bytes, None, None]:
        with open(file_path, "rb") as f:
            f.seek(start)
            remaining = end - start + 1
            while remaining > 0:
                read_size = min(chunk_size, remaining)
                data = f.read(read_size)
                if not data:
                    break
                remaining -= len(data)
                yield data

    headers = {
        "Content-Range": f"bytes {start}-{end}/{file_size}",
        "Accept-Ranges": "bytes",
        "Content-Length": str(end - start + 1),
        "Content-Disposition": f'inline; filename="{file_path.name}"',
    }

    status_code = 206 if range else 200
    return StreamingResponse(iter_file(), status_code=status_code, media_type=content_type, headers=headers)


@router.put("/upload")
async def upload_file(
    request: Request,
    path: str = Query(..., description="File path to upload to"),
) -> Response:
    """Upload a file to the workspace."""
    file_path = join_under(request.app.state.topology.workspace, Path(path))

    # Ensure parent directory exists
    file_path.parent.mkdir(parents=True, exist_ok=True)

    # Write the uploaded file content
    try:
        content = await request.body()
        with open(file_path, "wb") as f:
            f.write(content)

        return Response(
            status_code=201,
            content=json.dumps({"message": "File uploaded successfully", "path": str(Path(path))}),
            media_type="application/json",
        )
    except Exception as e:
        raise HTTPException(
            status_code=500,
            detail=PathError(message=f"Failed to upload file: {str(e)}", path=str(file_path)).model_dump(),
        )


@router.get("")
def list_files(
    request: Request,
    path: str = Query(..., description="Directory path to list files from"),
    include_hidden: bool = Query(False, description="Whether to include hidden files"),
) -> ListFilesResponse:
    try:
        root = join_under(request.app.state.topology.workspace, Path(path))
    except ValueError:
        raise HTTPException(status_code=400, detail=PathError(message="Invalid path", path=path).model_dump())

    if not root.exists():
        raise HTTPException(status_code=404, detail=PathError(message="Path not found", path=str(root)).model_dump())
    if not root.is_dir():
        raise HTTPException(status_code=400, detail=PathError(message="Path is not a directory", path=str(root)).model_dump())

    files = [describe_path(request.app.state.topology.workspace, p) for p in root.iterdir() if include_hidden or not p.name.startswith(".")]

    return ListFilesResponse(files=files)


TEXT_FILE_EXTENSIONS = set(
    [
        ".txt",
        ".md",
        ".json",
        ".yaml",
        ".yml",
        ".xml",
        ".csv",
        ".log",
        ".py",
        ".js",
        ".ts",
        ".tsx",
        ".jsx",
        ".html",
        ".css",
        ".scss",
        ".sh",
        ".bash",
        ".zsh",
        ".conf",
        ".cfg",
        ".ini",
        ".toml",
        ".sql",
        ".graphql",
        ".proto",
        ".kt",
        ".java",
        ".go",
        ".rs",
        ".c",
        ".cpp",
        ".h",
        ".hpp",
        ".swift",
        ".rb",
        ".lua",
    ]
)


def get_file_type(path: Path) -> FileType:  # noqa: PLR0911 C901
    if path.is_dir():
        return FileType.FOLDER

    name = path.name
    if name.endswith(".instance.yaml"):
        return FileType.INSTANCE
    if name.endswith(".platform.yaml"):
        return FileType.PLATFORM
    if name.endswith(".recordingsession.yaml"):
        return FileType.RECORDING_SESSION
    if name.endswith(".mapping.yaml"):
        return FileType.RECORDING_MAPPING
    if name.endswith(".dbc") or name.endswith(".arxml") or name.endswith(".ldf") or name.endswith(".xml"):
        return FileType.SIGNAL_DATABASE
    if name.endswith(".log"):
        return FileType.RECORDING
    if name.endswith(".mp4"):
        return FileType.VIDEO
    if name.endswith(".dashboard.json"):
        return FileType.DASHBOARD

    if path.suffix.lower() in TEXT_FILE_EXTENSIONS:
        return FileType.TEXT

    return FileType.UNKNOWN


def run_cmd_and_get_yaml(
    command: list[str],
    request: Request,
    path: str,
) -> Response:
    file_path = join_under(request.app.state.topology.workspace, Path(path))
    if not file_path.exists():
        raise HTTPException(status_code=404, detail=PathError(message="File not found", path=str(file_path)).model_dump())
    if not file_path.is_file():
        raise HTTPException(status_code=400, detail=PathError(message="Not a file", path=str(file_path)).model_dump())
    try:
        result = run_topology(request.app.state.topology, command + [str(file_path)])
        # TODO: use file output instead of stdout
        return Response(content=result.stdout, media_type="application/yaml")
    except subprocess.CalledProcessError as e:
        if e.stderr:
            stderr_str = e.stderr.decode("utf-8") if isinstance(e.stderr, bytes) else e.stderr
            try:
                error_detail = json.loads(stderr_str)
                raise HTTPException(status_code=400, detail=error_detail)
            except json.JSONDecodeError:
                raise HTTPException(status_code=500, detail=stderr_str)
        if e.stdout:
            raise HTTPException(status_code=500, detail=e.stdout)
        raise HTTPException(status_code=500, detail=f"Unexpected error: {str(e)}")
