from __future__ import annotations

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

from .client import PolarionClient
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,
)
from polarion_rest_client.openapi.api.projects.get_projects import (
    sync_detailed as _get_projects,
)
from polarion_rest_client.openapi.api.projects.patch_project import (
    sync_detailed as _patch_project,
)
from polarion_rest_client.openapi.api.projects.delete_project import (
    sync_detailed as _delete_project,
)
from polarion_rest_client.openapi.api.projects.create_project import (
    sync_detailed as _create_project,
)
from polarion_rest_client.openapi.api.projects.unmark_project import (
    sync_detailed as _unmark_project,
)
from polarion_rest_client.openapi.api.projects.mark_project import (
    sync_detailed as _mark_project,
)
from polarion_rest_client.openapi.api.project_templates.get_project_templates import (
    sync_detailed as _get_project_templates,
)
from polarion_rest_client.openapi.api.jobs.get_job import sync_detailed as _get_job

# 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:
    """
    High-level Project wrapper built on the generated low-level client.

    Supported operations:
      - list (GET /projects)
      - get (GET /projects/{projectId})
      - create (POST /projects/actions/createProject)  → async job
      - patch (PATCH /projects/{projectId})
      - delete (DELETE /projects/{projectId})          → async job
      - unmark (POST /projects/{projectId}/actions/unmarkProject)
      - mark   (POST /projects/actions/markProject)    → create from existing location
      - list_templates (GET /projecttemplates)
      - exists (HEAD via get handling)
    """

    def __init__(self, pc: PolarionClient) -> None:
        self._c = pc.gen  # AuthenticatedClient (generated)

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

    def _to_dict(self, resp) -> Dict[str, Any]:
        """Return parsed body as dict regardless of model/dict form."""
        if resp.parsed is None:
            return {}
        try:
            return resp.parsed.to_dict()
        except Exception:
            return resp.parsed  # already a dict

    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(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)

    # ── 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,
    ) -> 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}

        r = _get_projects(
            client=self._c,
            page_size=page_size,
            page_number=page_number,
            fields=fields_arg,
            include=include,
            query=query,
            sort=sort,
            revision=revision,
        )
        if r.status_code == 200 and r.parsed is not None:
            return self._to_dict(r)
        raise_from_response(r)

    def get(
        self,
        project_id: str,
        *,
        fields_projects: Optional[List[str]] = None,
        include: Optional[str] = None,
        revision: Optional[str] = None,
    ) -> 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})

        resp = _get_project(
            client=self._c,
            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,
        )
        if resp.status_code == 200 and resp.parsed:
            return self._to_dict(resp)
        raise_from_response(resp)

    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,
    ) -> 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 {}),
        })

        r = _create_project(client=self._c, json_body=body)
        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)
            if wait:
                self._wait_job(job_id, timeout_s=poll_timeout_s, poll_every_s=poll_every_s)
            return str(job_id)
        raise_from_response(r)

    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,
    ) -> 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)
        r = _patch_project(client=self._c, project_id=project_id, json_body=body)
        if r.status_code in (204,):
            return
        raise_from_response(r)

    def delete(
        self,
        project_id: str,
        *,
        wait: bool = True,
        poll_timeout_s: float = 180.0,
        poll_every_s: float = 2.0,
    ) -> str:
        """
        Delete a project (DELETE /projects/{projectId} → async job).
        Returns jobId. If wait=True, blocks until completion.
        """
        r = _delete_project(client=self._c, project_id=project_id)
        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)
            if wait:
                self._wait_job(job_id, timeout_s=poll_timeout_s, poll_every_s=poll_every_s)
            return str(job_id)
        raise_from_response(r)

    def unmark(self, project_id: str) -> None:
        """POST /projects/{projectId}/actions/unmarkProject"""
        r = _unmark_project(client=self._c, project_id=project_id)
        if r.status_code in (204,):
            return
        raise_from_response(r)

    def mark(
        self,
        *,
        project_id: str,
        location: str,
        tracker_prefix: str,
        name: Optional[str] = None,
        description: Optional[str] = None,
    ) -> 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

        # Many gens model it as free-form JSON; pass via **kwargs/json_body when available
        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 r.status_code in (204, 201, 202):
            # Some servers return 204, some 201/202 (job); handle all as success.
            return
        raise_from_response(r)

    def list_templates(self) -> Dict[str, Any]:
        """GET /projecttemplates (returns JSON:API doc with data[]=template resources)"""
        r = _get_project_templates(client=self._c)
        if r.status_code == 200 and r.parsed is not None:
            return self._to_dict(r)
        raise_from_response(r)

    def exists(self, project_id: str) -> bool:
        try:
            self.get(project_id, fields_projects=["id"])
            return True
        except JSONAPIError as e:
            if str(getattr(e, "status", "")) in ("404", 404):
                return False
            raise
