"""Async artifact file handling for Hypha."""

import io
import locale
import os
from collections.abc import Callable, Awaitable
from typing import Self
from types import TracebackType
import httpx
from .utils import FileMode


class AsyncArtifactHttpFile:
    """An async file-like object that supports async context manager protocols.

    This implements an async file interface for Hypha artifacts, handling HTTP operations
    via the httpx library.
    """

    name: str | None
    mode: str

    def __init__(
        self: Self,
        url_func: Callable[[], Awaitable[str]],
        mode: FileMode = "r",
        encoding: str | None = None,
        newline: str | None = None,
        name: str | None = None,
    ) -> None:
        self._url_func = url_func
        self._url: str | None = None
        self._pos = 0
        self._mode = mode
        self._encoding = encoding or locale.getpreferredencoding()
        self._newline = newline or os.linesep
        self.name = name
        self._closed = False
        self._buffer = io.BytesIO()
        self._client: httpx.AsyncClient | None = None

        if "r" in mode:
            self._size = 0  # Will be set when content is downloaded
        else:
            # For write modes, initialize an empty buffer
            self._size = 0

    async def __aenter__(self: Self) -> Self:
        """Async context manager entry."""
        self._client = httpx.AsyncClient()
        if "r" in self._mode:
            await self._download_content()
        return self

    async def __aexit__(
        self: Self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: TracebackType | None,
    ) -> None:
        """Async context manager exit."""
        await self.close()

    async def get_url(self: Self) -> str:
        """Get the URL for this file."""
        if self._url is None:
            self._url = await self._url_func()
        return self._url

    def _get_client(self: Self) -> httpx.AsyncClient:
        """Get or create httpx client."""
        if self._client is None:
            self._client = httpx.AsyncClient()
        return self._client

    async def _download_content(self: Self, range_header: str | None = None) -> None:
        """Download content from URL into buffer, optionally using a range header."""
        try:
            url = await self.get_url()

            headers: dict[str, str] = {
                "Accept-Encoding": "identity"  # Prevent gzip compression
            }
            if range_header:
                headers["Range"] = range_header

            client = self._get_client()
            response = await client.get(url, headers=headers, timeout=60)
            response.raise_for_status()
            self._buffer = io.BytesIO(response.content)
            self._size = len(response.content)
        except httpx.RequestError as e:
            # More detailed error information for debugging
            status_code = (
                getattr(e.request, "status_code", "unknown")
                if hasattr(e, "request")
                else "unknown"
            )
            message = str(e)
            raise IOError(
                f"Error downloading content (status {status_code}): {message}"
            ) from e
        except Exception as e:
            raise IOError(f"Unexpected error downloading content: {str(e)}") from e

    async def _upload_content(self: Self) -> httpx.Response:
        """Upload buffer content to URL"""
        try:
            content = self._buffer.getvalue()
            url = await self.get_url()

            headers = {
                "Content-Type": "", # Important for s3 compatibility
                # "Content-Type": "application/octet-stream",
                "Content-Length": str(len(content)),
            }

            client = self._get_client()
            response = await client.put(
                url, content=content, headers=headers, timeout=60
            )

            response.raise_for_status()
            return response
        except httpx.HTTPStatusError as e:
            status_code = (
                e.response.status_code if hasattr(e, "response") else "unknown"
            )
            error_msg = e.response.text if hasattr(e, "response") else str(e)
            raise IOError(
                f"HTTP error uploading content (status {status_code}): {error_msg}"
            ) from e
        except Exception as e:
            raise IOError(f"Error uploading content: {str(e)}") from e

    def tell(self: Self) -> int:
        """Return current position in the file"""
        return self._pos

    def seek(self: Self, offset: int, whence: int = 0) -> int:
        """Change stream position"""
        if whence == 0:  # os.SEEK_SET
            self._pos = offset
        elif whence == 1:  # os.SEEK_CUR
            self._pos += offset
        elif whence == 2:  # os.SEEK_END
            self._pos = self._size + offset

        # Make sure buffer's position is synced
        self._buffer.seek(self._pos)
        return self._pos

    async def read(self: Self, size: int = -1) -> bytes | str:
        """Read up to size bytes from the file, using HTTP range if necessary."""
        if "r" not in self._mode:
            raise IOError("File not open for reading")

        if size < 0:
            await self._download_content()
        else:
            range_header = f"bytes={self._pos}-{self._pos + size - 1}"
            await self._download_content(range_header=range_header)

        data = self._buffer.read()
        self._pos += len(data)

        if "b" not in self._mode:
            return data.decode(self._encoding)
        return data

    async def write(self: Self, data: str | bytes) -> int:
        """Write data to the file"""
        if "w" not in self._mode and "a" not in self._mode:
            raise IOError("File not open for writing")

        # Convert string to bytes if necessary
        if isinstance(data, str) and "b" in self._mode:
            data = data.encode(self._encoding)
        elif isinstance(data, bytes) and "b" not in self._mode:
            data = data.decode(self._encoding)
            data = data.encode(self._encoding)

        # Ensure we're at the right position
        self._buffer.seek(self._pos)

        # Write the data
        if isinstance(data, str):
            data = data.encode(self._encoding)
        bytes_written = self._buffer.write(data)
        self._pos += bytes_written
        self._size = max(self._size, self._pos)

        return bytes_written

    async def close(self: Self) -> None:
        """Close the file and upload content if in write mode"""
        if self._closed:
            return

        try:
            if ("w" in self._mode or "a" in self._mode) and self._buffer.tell() > 0:
                await self._upload_content()
        finally:
            self._closed = True
            self._buffer.close()
            if self._client:
                await self._client.aclose()

    @property
    def closed(self: Self) -> bool:
        """Return whether the file is closed"""
        return self._closed

    def readable(self: Self) -> bool:
        """Return whether the file is readable"""
        return "r" in self._mode

    def writable(self: Self) -> bool:
        """Return whether the file is writable"""
        return "w" in self._mode or "a" in self._mode

    def seekable(self: Self) -> bool:
        """Return whether the file supports seeking"""
        return True
