from __future__ import annotations

import time
import asyncio
import uuid
from typing import Any, Dict, List, Iterable, Mapping, Optional, Union, Awaitable

from .client import PolarionClient, PolarionAsyncClient
from .resource import PolarionResource
from .error import JSONAPIError, raise_from_response

# ── Low-level OpenAPI client imports (generated by openapi-python-client) ──
# Endpoints
from polarion_rest_client.openapi.types import UNSET
from polarion_rest_client.openapi.api.projects.get_project import (
    sync_detailed as _get_project,
    asyncio_detailed as _get_project_async,
)
from polarion_rest_client.openapi.api.projects.get_projects import (
    sync_detailed as _get_projects,
    asyncio_detailed as _get_projects_async,
)
from polarion_rest_client.openapi.api.projects.patch_project import (
    sync_detailed as _patch_project,
    asyncio_detailed as _patch_project_async,
)
from polarion_rest_client.openapi.api.projects.delete_project import (
    sync_detailed as _delete_project,
    asyncio_detailed as _delete_project_async,
)
from polarion_rest_client.openapi.api.projects.create_project import (
    sync_detailed as _create_project,
    asyncio_detailed as _create_project_async,
)
from polarion_rest_client.openapi.api.projects.unmark_project import (
    sync_detailed as _unmark_project,
    asyncio_detailed as _unmark_project_async,
)
from polarion_rest_client.openapi.api.projects.mark_project import (
    sync_detailed as _mark_project,
    asyncio_detailed as _mark_project_async,
)
from polarion_rest_client.openapi.api.project_templates.get_project_templates import (
    sync_detailed as _get_project_templates,
    asyncio_detailed as _get_project_templates_async,
)
from polarion_rest_client.openapi.api.jobs.get_job import (
    sync_detailed as _get_job,
    asyncio_detailed as _get_job_async,
)

# Models
from polarion_rest_client.openapi.models.create_project_request_body import (
    CreateProjectRequestBody,
)
from polarion_rest_client.openapi.models.projects_single_patch_request import (
    ProjectsSinglePatchRequest,
)

# Optional convenience for sparse fields (deepObject)
try:
    from polarion_rest_client.openapi.models.sparse_fields import SparseFields
except Exception:  # pragma: no cover - older gens may use a different name
    SparseFields = None  # type: ignore


class Project(PolarionResource):
    """
    High-level Project wrapper built on the generated low-level client.
    Supports both synchronous and asynchronous clients.
    """

    def __init__(self, pc: Union[PolarionClient, PolarionAsyncClient]) -> None:
        super().__init__(pc)
        self._c = pc.gen  # AuthenticatedClient (generated)

    # ── Helpers ──────────────────────────────────────────────────────────────

    def _extract_job_id(self, doc: Dict[str, Any]) -> Optional[str]:
        data = (doc or {}).get("data") or {}
        return data.get("id") or ((data.get("attributes") or {}).get("jobId"))

    def _wait_job_sync(
        self, job_id: str, *, timeout_s: float = 180.0, poll_every_s: float = 2.0
    ) -> None:
        """Poll GET /jobs/{jobId} until status.type ∈ {OK, FAILED, CANCELLED} or timeout."""
        deadline = time.monotonic() + timeout_s
        last_state = None
        while True:
            r = _get_job(client=self._c, job_id=job_id)
            if r.status_code == 200 and r.parsed:
                body = self._to_dict(r)
                attrs = ((body.get("data") or {}).get("attributes") or {})
                status = (attrs.get("status") or {}).get("type")
                last_state = status or attrs.get("state")
                if status in {"OK", "FAILED", "CANCELLED"}:
                    if status != "OK":
                        raise_from_response(r)  # convert to typed JSON:API error
                    return
            elif r.status_code >= 400:
                raise_from_response(r)

            if time.monotonic() >= deadline:
                # synthesize a JSON:API error document and raise via common helper
                fake = type("Resp", (), {
                    "status_code": 504,
                    "parsed": {"errors": [{"status": "504", "title": "Gateway Timeout",
                                           "detail": f"Job {job_id} did not finish within {timeout_s}s "
                                                     f"(last={last_state!r})"}]},
                    "content": b"",
                })()
                raise_from_response(fake)
            time.sleep(poll_every_s)

    async def _wait_job_async(
        self, job_id: str, *, timeout_s: float = 180.0, poll_every_s: float = 2.0
    ) -> None:
        """Poll GET /jobs/{jobId} until status.type ∈ {OK, FAILED, CANCELLED} or timeout."""
        deadline = time.monotonic() + timeout_s
        last_state = None
        while True:
            r = await _get_job_async(client=self._c, job_id=job_id)
            if r.status_code == 200 and r.parsed:
                body = self._to_dict(r)
                attrs = ((body.get("data") or {}).get("attributes") or {})
                status = (attrs.get("status") or {}).get("type")
                last_state = status or attrs.get("state")
                if status in {"OK", "FAILED", "CANCELLED"}:
                    if status != "OK":
                        raise_from_response(r)
                    return
            elif r.status_code >= 400:
                raise_from_response(r)

            if time.monotonic() >= deadline:
                fake = type("Resp", (), {
                    "status_code": 504,
                    "parsed": {"errors": [{"status": "504", "title": "Gateway Timeout",
                                           "detail": f"Job {job_id} did not finish within {timeout_s}s "
                                                     f"(last={last_state!r})"}]},
                    "content": b"",
                })()
                raise_from_response(fake)
            await asyncio.sleep(poll_every_s)

    # ── Public API ───────────────────────────────────────────────────────────

    def list(
        self,
        *,
        page_size: Optional[int] = None,
        page_number: Optional[int] = None,
        fields_projects: Optional[Iterable[str]] = None,
        include: Optional[str] = None,
        query: Optional[str] = None,
        sort: Optional[str] = None,
        revision: Optional[str] = None,
    ) -> Union[Dict[str, Any], Awaitable[Dict[str, Any]]]:
        """
        List projects. Returns the full JSON:API document (dict).

        fields_projects: if provided, will be used to build SparseFields(projects="id,name,...")
        """
        fields_arg = None
        if fields_projects:
            csv = ",".join(fields_projects)
            if SparseFields is not None:
                fields_arg = SparseFields.from_dict({"projects": csv})  # type: ignore[attr-defined]
            else:
                # Older gens may accept a plain dict for deepObject
                fields_arg = {"projects": csv}

        return self._request(
            _get_projects,
            _get_projects_async,
            page_size=page_size,
            page_number=page_number,
            fields=fields_arg,
            include=include,
            query=query,
            sort=sort,
            revision=revision,
        )

    def get(
        self,
        project_id: str,
        *,
        fields_projects: Optional[List[str]] = None,
        include: Optional[str] = None,
        revision: Optional[str] = None,
    ) -> Union[Dict[str, Any], Awaitable[Dict[str, Any]]]:
        """
        GET /projects/{projectId}
        - fields_projects: list of attribute names for sparse fields on 'projects'
        """
        # Build SparseFields only if caller provided fields; otherwise leave UNSET
        fields = UNSET
        if fields_projects:
            if isinstance(fields_projects, str):
                proj_fields = [p.strip() for p in fields_projects.split(",") if p.strip()]
            else:
                proj_fields = list(fields_projects)
            fields = SparseFields.from_dict({"projects": proj_fields})

        return self._request(
            _get_project,
            _get_project_async,
            project_id=project_id,
            fields=fields,                                # UNSET if not provided
            include=include if include is not None else UNSET,
            revision=revision if revision is not None else UNSET,
        )

    def create(
        self,
        project_id: Optional[str] = None,
        *,
        tracker_prefix: Optional[str] = None,
        location: Optional[str] = None,
        template_id: Optional[str] = None,
        params: Optional[Mapping[str, Any]] = None,
        wait: bool = True,
        poll_timeout_s: float = 180.0,
        poll_every_s: float = 2.0,
    ) -> Union[str, Awaitable[str]]:
        """
        Create a project (POST /projects/actions/createProject → async job).
        Returns the jobId. If wait=True, blocks until completion.
        """
        if not project_id:
            project_id = f"tmp-{uuid.uuid4().hex[:10]}"

        body = CreateProjectRequestBody.from_dict({
            "projectId": project_id,
            **({"trackerPrefix": tracker_prefix} if tracker_prefix else {}),
            **({"location": location} if location else {}),
            **({"templateId": template_id} if template_id else {}),
            **({"params": dict(params)} if params else {}),
        })

        # Inner logic logic to reuse in sync/async blocks
        def _process_creation_response(r):
            if r.status_code in (201, 202) and r.parsed:
                job_id = self._extract_job_id(self._to_dict(r))
                if not job_id:
                    raise_from_response(r)
                return str(job_id)
            raise_from_response(r)

        if self.client.is_async:
            async def _do_async():
                r = await _create_project_async(client=self._c, json_body=body)
                job_id = _process_creation_response(r)
                if wait:
                    await self._wait_job_async(job_id, timeout_s=poll_timeout_s, poll_every_s=poll_every_s)
                return job_id
            return _do_async()
        else:
            r = _create_project(client=self._c, json_body=body)
            job_id = _process_creation_response(r)
            if wait:
                self._wait_job_sync(job_id, timeout_s=poll_timeout_s, poll_every_s=poll_every_s)
            return job_id

    def patch(
        self,
        project_id: str,
        *,
        name: Optional[str] = None,
        description: Optional[str] = None,  # plain text
        color: Optional[str] = None,
        active: Optional[bool] = None,
        start: Optional[str] = None,   # YYYY-MM-DD
        finish: Optional[str] = None,  # YYYY-MM-DD
        tracker_prefix: Optional[str] = None,
        lead_user_id: Optional[str] = None,
        lock_work_records_date: Optional[str] = None,  # YYYY-MM-DD
        icon: Optional[str] = None,
    ) -> Union[None, Awaitable[None]]:
        """
        Update a project via PATCH /projects/{projectId}. Only provided fields are sent.
        """
        attributes: Dict[str, Any] = {}
        if name is not None:
            attributes["name"] = name
        if description is not None:
            attributes["description"] = {"type": "text/plain", "value": description}
        if color is not None:
            attributes["color"] = color
        if active is not None:
            attributes["active"] = bool(active)
        if start is not None:
            attributes["start"] = start
        if finish is not None:
            attributes["finish"] = finish
        if tracker_prefix is not None:
            attributes["trackerPrefix"] = tracker_prefix
        if lock_work_records_date is not None:
            attributes["lockWorkRecordsDate"] = lock_work_records_date
        if icon is not None:
            attributes["icon"] = icon

        relationships: Dict[str, Any] = {}
        if lead_user_id is not None:
            relationships["lead"] = {"data": {"type": "users", "id": lead_user_id}}

        payload: Dict[str, Any] = {
            "data": {
                "type": "projects",
                "id": project_id,
                **({"attributes": attributes} if attributes else {}),
                **({"relationships": relationships} if relationships else {}),
            }
        }

        body = ProjectsSinglePatchRequest.from_dict(payload)
        return self._request_no_content(
            _patch_project,
            _patch_project_async,
            project_id=project_id,
            json_body=body
        )

    def delete(
        self,
        project_id: str,
        *,
        wait: bool = True,
        poll_timeout_s: float = 180.0,
        poll_every_s: float = 2.0,
    ) -> Union[str, Awaitable[str]]:
        """
        Delete a project (DELETE /projects/{projectId} → async job).
        Returns jobId. If wait=True, blocks until completion.
        """
        def _process_delete_response(r):
            if r.status_code in (202,) and r.parsed:
                job_id = self._extract_job_id(self._to_dict(r))
                if not job_id:
                    raise_from_response(r)
                return str(job_id)
            raise_from_response(r)

        if self.client.is_async:
            async def _do_async():
                r = await _delete_project_async(client=self._c, project_id=project_id)
                job_id = _process_delete_response(r)
                if wait:
                    await self._wait_job_async(job_id, timeout_s=poll_timeout_s, poll_every_s=poll_every_s)
                return job_id
            return _do_async()
        else:
            r = _delete_project(client=self._c, project_id=project_id)
            job_id = _process_delete_response(r)
            if wait:
                self._wait_job_sync(job_id, timeout_s=poll_timeout_s, poll_every_s=poll_every_s)
            return job_id

    def unmark(self, project_id: str) -> Union[None, Awaitable[None]]:
        """POST /projects/{projectId}/actions/unmarkProject"""
        return self._request_no_content(
            _unmark_project,
            _unmark_project_async,
            project_id=project_id
        )

    def mark(
        self,
        *,
        project_id: str,
        location: str,
        tracker_prefix: str,
        name: Optional[str] = None,
        description: Optional[str] = None,
    ) -> Union[None, Awaitable[None]]:
        """
        POST /projects/actions/markProject  (register an existing repository location as a project)
        """
        # markProject uses query/body per generator; most recent specs take JSON body
        payload: Dict[str, Any] = {
            "projectId": project_id,
            "location": location,
            "trackerPrefix": tracker_prefix,
        }
        if name:
            payload["name"] = name
        if description:
            payload["description"] = description

        if self.client.is_async:
            async def _do_async():
                try:
                    r = await _mark_project_async(client=self._c, json_body=payload)
                except TypeError:
                    r = await _mark_project_async(client=self._c, body=payload)
                if not (200 <= r.status_code < 300):
                    raise_from_response(r)
            return _do_async()
        else:
            try:
                r = _mark_project(client=self._c, json_body=payload)  # newest gens
            except TypeError:
                r = _mark_project(client=self._c, body=payload)  # older gens

            if not (200 <= r.status_code < 300):
                raise_from_response(r)

    def list_templates(self) -> Union[Dict[str, Any], Awaitable[Dict[str, Any]]]:
        """GET /projecttemplates (returns JSON:API doc with data[]=template resources)"""
        return self._request(
            _get_project_templates,
            _get_project_templates_async,
        )

    @staticmethod
    def _is_404(e: JSONAPIError) -> bool:
        status = getattr(e, "status", None)
        # Try int first
        try:
            return int(status) == 404
        except (TypeError, ValueError):
            return str(status) == "404"

    def exists(self, project_id: str) -> Union[bool, Awaitable[bool]]:
        if self.client.is_async:
            async def _do_async():
                try:
                    await self.get(project_id, fields_projects=["id"])
                    return True
                except JSONAPIError as e:
                    if self._is_404(e):
                        return False
                    raise
            return _do_async()
        else:
            try:
                self.get(project_id, fields_projects=["id"])
                return True
            except JSONAPIError as e:
                if self._is_404(e):
                    return False
                raise
