"""Microsoft To Do provider adapter (Graph API OAuth mode)."""

from __future__ import annotations

from datetime import datetime, timezone
import os
from typing import Any

import requests
from requests.exceptions import RequestException

from app.services.integrations.errors import ProviderConnectionError, ProviderNotConfiguredError
from app.services.integrations.types import ProviderConnectionResult, UnifiedTask

REQUEST_TIMEOUT_SECONDS = 20
GRAPH_API_BASE = "https://graph.microsoft.com/v1.0"
MICROSOFT_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token"


class MicrosoftTodoProvider:
    """Microsoft Graph To Do adapter."""

    provider_id = "microsoft_todo"
    display_name = "Microsoft To Do"

    def __init__(self, *, access_token: str, refresh_token: str | None = None, list_id: str | None = None):
        self._access_token = (access_token or "").strip()
        self._refresh_token = (refresh_token or "").strip() or None
        self._list_id = (list_id or "").strip() or None
        if not self._access_token and not self._refresh_token:
            raise ProviderNotConfiguredError("Microsoft To Do OAuth token is required")

    def _headers(self) -> dict[str, str]:
        if not self._access_token:
            raise ProviderNotConfiguredError("Microsoft access token is missing")
        return {
            "Authorization": f"Bearer {self._access_token}",
            "Content-Type": "application/json",
        }

    def _refresh_access_token(self) -> None:
        if not self._refresh_token:
            raise ProviderConnectionError("Microsoft refresh token not available")
        client_id = (os.environ.get("MICROSOFT_OAUTH_CLIENT_ID") or "").strip()
        client_secret = (os.environ.get("MICROSOFT_OAUTH_CLIENT_SECRET") or "").strip()
        if not client_id or not client_secret:
            raise ProviderConnectionError("Microsoft OAuth client credentials are missing in environment")

        data = {
            "client_id": client_id,
            "client_secret": client_secret,
            "refresh_token": self._refresh_token,
            "grant_type": "refresh_token",
            "scope": "Tasks.ReadWrite offline_access",
        }
        try:
            response = requests.post(MICROSOFT_TOKEN_URL, data=data, timeout=REQUEST_TIMEOUT_SECONDS)
        except RequestException as exc:
            raise ProviderConnectionError(f"Microsoft token refresh failed: {exc}") from exc
        if response.status_code >= 400:
            raise ProviderConnectionError(f"Microsoft token refresh failed ({response.status_code}): {response.text.strip()}")
        try:
            body = response.json()
        except ValueError as exc:
            raise ProviderConnectionError("Microsoft token refresh returned invalid JSON") from exc
        token = str(body.get("access_token", "")).strip()
        if not token:
            raise ProviderConnectionError("Microsoft token refresh did not return access_token")
        self._access_token = token

    def _request(self, method: str, path: str, *, json_payload: dict[str, Any] | None = None) -> dict[str, Any] | list[Any]:
        url = f"{GRAPH_API_BASE}{path}"

        def _send() -> requests.Response:
            return requests.request(
                method=method,
                url=url,
                headers=self._headers(),
                json=json_payload,
                timeout=REQUEST_TIMEOUT_SECONDS,
            )

        try:
            response = _send()
        except RequestException as exc:
            raise ProviderConnectionError(f"Microsoft Graph request failed: {exc}") from exc

        if response.status_code == 401 and self._refresh_token:
            self._refresh_access_token()
            try:
                response = _send()
            except RequestException as exc:
                raise ProviderConnectionError(f"Microsoft Graph retry failed: {exc}") from exc

        if response.status_code >= 400:
            detail = response.text.strip() or "Microsoft Graph API request failed"
            raise ProviderConnectionError(f"Microsoft Graph API error ({response.status_code}): {detail}")

        try:
            return response.json() if response.text.strip() else {}
        except ValueError as exc:
            raise ProviderConnectionError("Microsoft Graph returned invalid JSON") from exc

    def _resolve_list_id(self) -> str:
        if self._list_id:
            return self._list_id
        payload = self._request("GET", "/me/todo/lists")
        items = payload.get("value") if isinstance(payload, dict) else None
        if isinstance(items, list) and items:
            first = items[0]
            if isinstance(first, dict):
                resolved = str(first.get("id", "")).strip()
                if resolved:
                    self._list_id = resolved
                    return resolved
        raise ProviderNotConfiguredError("No Microsoft To Do list available")

    @staticmethod
    def _to_task(item: dict[str, Any]) -> UnifiedTask:
        due_obj = item.get("dueDateTime") if isinstance(item.get("dueDateTime"), dict) else {}
        due_date = str(due_obj.get("dateTime", "")).strip()[:10] or None
        body_obj = item.get("body") if isinstance(item.get("body"), dict) else {}
        content = str(item.get("title", "")).strip() or "Untitled task"
        description = str(body_obj.get("content", "")).strip() or None
        status = str(item.get("status", "")).strip().lower()
        return UnifiedTask(
            provider="microsoft_todo",
            id=str(item.get("id", "")),
            title=content,
            description=description,
            due_date=due_date,
            due_string=due_date,
            completed=status == "completed",
            raw={"status": item.get("status")},
        )

    def list_tasks(self, *, include_completed: bool = False) -> list[UnifiedTask]:
        list_id = self._resolve_list_id()
        payload = self._request("GET", f"/me/todo/lists/{list_id}/tasks")
        items = payload.get("value") if isinstance(payload, dict) else None
        if not isinstance(items, list):
            return []
        tasks = [self._to_task(item) for item in items if isinstance(item, dict)]
        if include_completed:
            return tasks
        return [item for item in tasks if not item.completed]

    def create_task(self, *, content: str, description: str | None = None, due_string: str | None = None) -> UnifiedTask:
        list_id = self._resolve_list_id()
        payload: dict[str, Any] = {"title": content}
        if description:
            payload["body"] = {"content": description, "contentType": "text"}
        if due_string:
            payload["dueDateTime"] = {"dateTime": f"{due_string}T00:00:00", "timeZone": "UTC"}
        item = self._request("POST", f"/me/todo/lists/{list_id}/tasks", json_payload=payload)
        if not isinstance(item, dict):
            raise ProviderConnectionError("Unexpected Microsoft To Do response")
        return self._to_task(item)

    def complete_task(self, *, task_id: str) -> None:
        list_id = self._resolve_list_id()
        _ = self._request(
            "PATCH",
            f"/me/todo/lists/{list_id}/tasks/{task_id}",
            json_payload={"status": "completed"},
        )

    def update_due(self, *, task_id: str, due_string: str) -> UnifiedTask | None:
        list_id = self._resolve_list_id()
        item = self._request(
            "PATCH",
            f"/me/todo/lists/{list_id}/tasks/{task_id}",
            json_payload={"dueDateTime": {"dateTime": f"{due_string}T00:00:00", "timeZone": "UTC"}},
        )
        if not isinstance(item, dict):
            return None
        return self._to_task(item)

    def test_connection(self) -> ProviderConnectionResult:
        try:
            payload = self._request("GET", "/me")
        except Exception as exc:
            return ProviderConnectionResult(ok=False, detail=str(exc))
        if isinstance(payload, dict):
            label = payload.get("userPrincipalName") or payload.get("displayName") or "user"
            return ProviderConnectionResult(ok=True, detail=f"Connected as {label} ({datetime.now(timezone.utc).isoformat()})")
        return ProviderConnectionResult(ok=False, detail="Microsoft test returned unexpected payload")
