from __future__ import annotations

import asyncio
import logging
import os
import mimetypes
import pathlib
from typing import IO, Any, Dict, List, Optional, Sequence, Tuple, Union

import httpx

from .._files import extract_files, to_httpx_files
from .._models import FinalRequestOptions
from .._resource import AsyncAPIResource, SyncAPIResource
from .._types import NOT_GIVEN, FileInput, NotGiven, is_given
from .._utils import deepcopy_minimal, strip_not_given
from ..types.file_object import FileObject
from ..types.file_presign import PresignResponse

_FILE_ID_PREFIX = "file_"
_log = logging.getLogger("ponyflash.files")


def _read_input(file: FileInput) -> tuple[str, bytes, str]:
    """Return (filename, content_bytes, content_type) for a uploadable FileInput.

    Only handles non-string types (Path, IO, bytes, tuple).
    Strings (URLs and file_ids) are handled by resolve() before reaching here.
    """
    if isinstance(file, bytes):
        return ("upload", file, "application/octet-stream")
    if isinstance(file, tuple) and len(file) == 2:
        name, data = file
        ct = mimetypes.guess_type(name)[0] or "application/octet-stream"
        return (name, data, ct)
    if isinstance(file, os.PathLike):
        p = pathlib.Path(file)
        ct = mimetypes.guess_type(p.name)[0] or "application/octet-stream"
        return (p.name, p.read_bytes(), ct)
    if hasattr(file, "read"):
        name = getattr(file, "name", "upload")
        if isinstance(name, (os.PathLike, pathlib.PurePath)):
            name = pathlib.Path(name).name
        ct = mimetypes.guess_type(str(name))[0] or "application/octet-stream"
        data = file.read()  # type: ignore[union-attr]
        return (str(name), data, ct)
    if isinstance(file, str):
        raise TypeError(
            f"Unexpected string {file!r}. "
            f"URL strings (https://...) and file_id strings (file_...) are handled automatically. "
            f"For local files, use open('{file}', 'rb') or Path('{file}')."
        )
    raise TypeError(f"Unsupported FileInput type: {type(file)}")


def _is_url(v: object) -> bool:
    return isinstance(v, str) and (v.startswith("http://") or v.startswith("https://"))


def _is_file_id(v: object) -> bool:
    return isinstance(v, str) and v.startswith(_FILE_ID_PREFIX) and not _is_url(v)


class Files(SyncAPIResource):
    # ── upload ──

    def presign(self, *, filename: str, content_type: str, size: int) -> PresignResponse:
        return self._post(
            "/files/presign",
            cast_to=PresignResponse,
            body={"filename": filename, "content_type": content_type, "size": size},
        )

    def upload(self, file: FileInput) -> str:
        """Presign → upload to COS → return file_id."""
        name, data, ct = _read_input(file)
        presign = self.presign(filename=name, content_type=ct, size=len(data))
        httpx.put(
            presign.upload_url,
            content=data,
            headers={**presign.headers, "Content-Type": ct},
            timeout=300,
        )
        return presign.file_id

    # ── resolve (tracks temp uploads) ──

    def resolve(self, inp: FileInput, *, _temp: list[str] | None = None) -> dict[str, str]:
        """Resolve FileInput → API JSON. Appends auto-uploaded file_ids to *_temp*."""
        if _is_url(inp):
            return {"type": "url", "url": str(inp)}
        if _is_file_id(inp):
            return {"type": "file_id", "file_id": str(inp)}
        file_id = self.upload(inp)
        if _temp is not None:
            _temp.append(file_id)
        return {"type": "file_id", "file_id": file_id}

    def resolve_many(self, inputs: Sequence[FileInput], *, _temp: list[str] | None = None) -> list[dict[str, str]]:
        return [self.resolve(i, _temp=_temp) for i in inputs]

    # ── cleanup ──

    def cleanup(self, file_ids: Sequence[str]) -> None:
        """Best-effort delete of temporary file_ids. Errors are logged, never raised."""
        for fid in file_ids:
            try:
                self._delete(f"/files/{fid}", cast_to=None)  # type: ignore[arg-type]
                _log.debug("Cleaned up temp file %s", fid)
            except Exception:
                _log.debug("Failed to clean up temp file %s (may already be expired)", fid, exc_info=True)

    # ── file management (still available for advanced users) ──

    def get(self, file_id: str) -> FileObject:
        return self._get(f"/files/{file_id}", cast_to=FileObject)

    def list(self) -> list[FileObject]:
        resp = self._get("/files", cast_to=dict)  # type: ignore[arg-type]
        items = resp.get("items", []) if isinstance(resp, dict) else []  # type: ignore[union-attr]
        return [FileObject.model_validate(item) for item in items]

    def delete(self, file_id: str) -> None:
        self._delete(f"/files/{file_id}", cast_to=None)  # type: ignore[arg-type]


class AsyncFiles(AsyncAPIResource):
    # ── upload ──

    async def presign(self, *, filename: str, content_type: str, size: int) -> PresignResponse:
        return await self._post(
            "/files/presign",
            cast_to=PresignResponse,
            body={"filename": filename, "content_type": content_type, "size": size},
        )

    async def upload(self, file: FileInput) -> str:
        name, data, ct = _read_input(file)
        presign = await self.presign(filename=name, content_type=ct, size=len(data))
        async with httpx.AsyncClient(timeout=300) as http:
            await http.put(
                presign.upload_url,
                content=data,
                headers={**presign.headers, "Content-Type": ct},
            )
        return presign.file_id

    # ── resolve ──

    async def resolve(self, inp: FileInput, *, _temp: list[str] | None = None) -> dict[str, str]:
        if _is_url(inp):
            return {"type": "url", "url": str(inp)}
        if _is_file_id(inp):
            return {"type": "file_id", "file_id": str(inp)}
        file_id = await self.upload(inp)
        if _temp is not None:
            _temp.append(file_id)
        return {"type": "file_id", "file_id": file_id}

    async def resolve_many(self, inputs: Sequence[FileInput], *, _temp: list[str] | None = None) -> list[dict[str, str]]:
        results = await asyncio.gather(*(self.resolve(i, _temp=_temp) for i in inputs))
        return list(results)

    # ── cleanup ──

    async def cleanup(self, file_ids: Sequence[str]) -> None:
        for fid in file_ids:
            try:
                await self._delete(f"/files/{fid}", cast_to=None)  # type: ignore[arg-type]
                _log.debug("Cleaned up temp file %s", fid)
            except Exception:
                _log.debug("Failed to clean up temp file %s (may already be expired)", fid, exc_info=True)

    # ── file management ──

    async def get(self, file_id: str) -> FileObject:
        return await self._get(f"/files/{file_id}", cast_to=FileObject)

    async def list(self) -> list[FileObject]:
        resp = await self._get("/files", cast_to=dict)  # type: ignore[arg-type]
        items = resp.get("items", []) if isinstance(resp, dict) else []  # type: ignore[union-attr]
        return [FileObject.model_validate(item) for item in items]

    async def delete(self, file_id: str) -> None:
        await self._delete(f"/files/{file_id}", cast_to=None)  # type: ignore[arg-type]
