"""Gitea release provider — queries ``/api/v1/repos/{owner}/{repo}/releases``."""

from __future__ import annotations

import asyncio
import logging
from typing import ClassVar

import aiohttp

from ..notifications.ssrf import UnsafeURLError, avalidate_outbound_url
from .base import (
    ReleaseErrorCode,
    ReleaseInfo,
    ReleaseProviderKind,
    ReleaseTestResult,
    is_valid_repo,
    normalise_version,
)

_LOGGER = logging.getLogger(__name__)

# Cap upstream response body — release lists are normally a few KB; anything
# beyond this is either a misconfigured target or a malicious payload.
_MAX_BODY_BYTES = 1_000_000


class GiteaReleaseProvider:
    """Anonymous Gitea release probe.

    Hits the ``releases`` endpoint (not ``releases/latest``) because the latter
    skips pre-releases unconditionally — we want to honour the caller's
    ``include_prereleases`` flag instead of relying on Gitea's filtering.
    """

    kind: ClassVar[ReleaseProviderKind] = ReleaseProviderKind.GITEA

    def __init__(self, session: aiohttp.ClientSession, url: str, repo: str) -> None:
        if not url:
            raise ValueError("Gitea release provider requires a base URL")
        if not is_valid_repo(repo):
            raise ValueError(
                "Gitea release provider requires repo as 'owner/name' "
                "(alphanumerics, dot, dash, underscore only)"
            )
        self._session = session
        self._url = url.rstrip("/")
        self._repo = repo.strip("/")

    @property
    def _endpoint(self) -> str:
        return f"{self._url}/api/v1/repos/{self._repo}/releases"

    async def fetch_latest(self, *, include_prereleases: bool = False) -> ReleaseInfo | None:
        try:
            await avalidate_outbound_url(self._endpoint)
        except UnsafeURLError as err:
            _LOGGER.warning("Gitea release URL rejected by SSRF guard: %s", err)
            return None

        try:
            async with self._session.get(
                self._endpoint,
                params={"limit": "20", "page": "1", "draft": "false"},
            ) as response:
                if response.status != 200:
                    _LOGGER.warning(
                        "Gitea releases fetch failed: HTTP %s for %s",
                        response.status, self._endpoint,
                    )
                    return None
                # Enforce a size cap without trusting chunked encoding: read
                # the whole body (aiohttp buffers it) but reject anything that
                # advertised more than the cap up front, and bail if it grew
                # past the cap after the fact.
                if response.content_length is not None and response.content_length > _MAX_BODY_BYTES:
                    _LOGGER.warning(
                        "Gitea releases response advertised %d bytes — refusing",
                        response.content_length,
                    )
                    return None
                raw = await response.read()
                if len(raw) > _MAX_BODY_BYTES:
                    _LOGGER.warning(
                        "Gitea releases response exceeded %d bytes — refusing to parse",
                        _MAX_BODY_BYTES,
                    )
                    return None
                import json

                payload = json.loads(raw.decode("utf-8"))
        except (aiohttp.ClientError, asyncio.TimeoutError) as err:
            _LOGGER.warning("Gitea releases fetch error: %s", err)
            return None
        except (ValueError, UnicodeDecodeError) as err:
            _LOGGER.warning("Gitea releases parse error: %s", err)
            return None

        if not isinstance(payload, list):
            return None

        for entry in payload:
            if not isinstance(entry, dict):
                continue
            if entry.get("draft"):
                continue
            if entry.get("prerelease") and not include_prereleases:
                continue
            return _to_release_info(entry)
        return None

    async def test(self) -> ReleaseTestResult:
        # Validate URL first so the "test" button surfaces an SSRF rejection
        # to the operator rather than silently returning "unreachable".
        try:
            await avalidate_outbound_url(self._endpoint)
        except UnsafeURLError:
            return {"ok": False, "info": None, "error": ReleaseErrorCode.UNSAFE_URL.value}

        try:
            async with self._session.get(
                self._endpoint,
                params={"limit": "1", "page": "1", "draft": "false"},
            ) as response:
                if response.status != 200:
                    return {"ok": False, "info": None, "error": ReleaseErrorCode.HTTP_ERROR.value}
                # Enforce a size cap without trusting chunked encoding: read
                # the whole body (aiohttp buffers it) but reject anything that
                # advertised more than the cap up front, and bail if it grew
                # past the cap after the fact.
                if response.content_length is not None and response.content_length > _MAX_BODY_BYTES:
                    _LOGGER.warning(
                        "Gitea releases response advertised %d bytes — refusing",
                        response.content_length,
                    )
                    return None
                raw = await response.read()
                if len(raw) > _MAX_BODY_BYTES:
                    return {"ok": False, "info": None, "error": ReleaseErrorCode.PARSE_ERROR.value}
                import json

                payload = json.loads(raw.decode("utf-8"))
        except (aiohttp.ClientError, asyncio.TimeoutError):
            return {"ok": False, "info": None, "error": ReleaseErrorCode.NETWORK_ERROR.value}
        except (ValueError, UnicodeDecodeError):
            return {"ok": False, "info": None, "error": ReleaseErrorCode.PARSE_ERROR.value}

        if not isinstance(payload, list) or not payload:
            return {"ok": False, "info": None, "error": ReleaseErrorCode.NO_RELEASE_FOUND.value}
        first = payload[0]
        if not isinstance(first, dict):
            return {"ok": False, "info": None, "error": ReleaseErrorCode.PARSE_ERROR.value}
        return {"ok": True, "info": _to_release_info(first), "error": None}


def _to_release_info(entry: dict) -> ReleaseInfo:
    tag = str(entry.get("tag_name") or "").strip()
    return ReleaseInfo(
        tag=tag,
        version=normalise_version(tag),
        name=entry.get("name") or None,
        body=entry.get("body") or None,
        url=entry.get("html_url") or None,
        published_at=entry.get("published_at") or entry.get("created_at") or None,
        prerelease=bool(entry.get("prerelease", False)),
        draft=bool(entry.get("draft", False)),
    )
