from __future__ import annotations

from datetime import datetime, timedelta, timezone
from typing import Any
from urllib.parse import urlencode

import requests

from .config import Settings


AUTHORIZE_URL = "https://www.patreon.com/oauth2/authorize"
TOKEN_URL = "https://www.patreon.com/api/oauth2/token"
IDENTITY_URL = "https://www.patreon.com/api/oauth2/v2/identity"
SCOPES = "identity identity.memberships"
USER_AGENT = "Flexible Survival RenPy Patreon Auth"


class PatreonClient:
    def __init__(self, settings: Settings) -> None:
        self.settings = settings

    def build_authorize_url(self, state_token: str) -> str:
        query = urlencode(
            {
                "response_type": "code",
                "client_id": self.settings.patreon_client_id,
                "redirect_uri": self.settings.patreon_redirect_uri,
                "scope": SCOPES,
                "state": state_token,
            }
        )
        return f"{AUTHORIZE_URL}?{query}"

    def exchange_code(self, code: str) -> dict[str, Any]:
        response = requests.post(
            TOKEN_URL,
            data={
                "code": code,
                "grant_type": "authorization_code",
                "client_id": self.settings.patreon_client_id,
                "client_secret": self.settings.patreon_client_secret,
                "redirect_uri": self.settings.patreon_redirect_uri,
            },
            headers={
                "Content-Type": "application/x-www-form-urlencoded",
                "User-Agent": USER_AGENT,
            },
            timeout=20,
        )
        response.raise_for_status()
        return response.json()

    def refresh_token(self, refresh_token: str) -> dict[str, Any]:
        response = requests.post(
            TOKEN_URL,
            data={
                "grant_type": "refresh_token",
                "refresh_token": refresh_token,
                "client_id": self.settings.patreon_client_id,
                "client_secret": self.settings.patreon_client_secret,
            },
            headers={
                "Content-Type": "application/x-www-form-urlencoded",
                "User-Agent": USER_AGENT,
            },
            timeout=20,
        )
        response.raise_for_status()
        return response.json()

    def fetch_identity_and_membership(self, access_token: str) -> dict[str, Any]:
        response = requests.get(
            IDENTITY_URL,
            params={
                "include": "memberships,memberships.currently_entitled_tiers",
                "fields[user]": "full_name",
                "fields[member]": (
                    "currently_entitled_amount_cents,patron_status,"
                    "last_charge_date,last_charge_status"
                ),
                "fields[tier]": "title,amount_cents,description,discord_role_ids",
            },
            headers={
                "Authorization": f"Bearer {access_token}",
                "User-Agent": USER_AGENT,
            },
            timeout=20,
        )
        response.raise_for_status()
        payload = response.json()
        return self._extract_entitlement(payload)

    def _extract_entitlement(self, payload: dict[str, Any]) -> dict[str, Any]:
        included = payload.get("included", [])
        included_index: dict[tuple[str, str], dict[str, Any]] = {}
        for item in included:
            included_index[(item.get("type", ""), item.get("id", ""))] = item

        membership_records = [
            item for item in included if item.get("type") == "member"
        ]

        selected_membership = None
        for membership in membership_records:
            campaign_rel = membership.get("relationships", {}).get("campaign", {}).get("data")
            if campaign_rel and str(campaign_rel.get("id")) == self.settings.patreon_campaign_id:
                selected_membership = membership
                break

        if selected_membership is None and len(membership_records) == 1:
            # Patreon docs indicate that without broader membership scope context,
            # the identity endpoint can still return only the membership to your campaign.
            selected_membership = membership_records[0]

        user_id = payload.get("data", {}).get("id")
        user_name = payload.get("data", {}).get("attributes", {}).get("full_name")

        if not selected_membership:
            return {
                "patreon_user_id": user_id,
                "patreon_user_name": user_name,
                "campaign_id": self.settings.patreon_campaign_id,
                "is_entitled": False,
                "patron_status": None,
                "last_charge_status": None,
                "last_charge_date": None,
                "currently_entitled_amount_cents": 0,
                "tier_ids": [],
                "tiers": [],
            }

        attrs = selected_membership.get("attributes", {})
        tier_refs = (
            selected_membership.get("relationships", {})
            .get("currently_entitled_tiers", {})
            .get("data", [])
        )
        tiers = []
        for tier_ref in tier_refs:
            tier = included_index.get((tier_ref.get("type", ""), tier_ref.get("id", "")))
            if not tier:
                tiers.append({"id": tier_ref.get("id")})
                continue
            tier_attrs = tier.get("attributes", {})
            tiers.append(
                {
                    "id": tier.get("id"),
                    "title": tier_attrs.get("title"),
                    "amount_cents": tier_attrs.get("amount_cents"),
                    "description": tier_attrs.get("description"),
                    "discord_role_ids": tier_attrs.get("discord_role_ids", []),
                }
            )

        patron_status = attrs.get("patron_status")
        amount_cents = attrs.get("currently_entitled_amount_cents") or 0
        return {
            "patreon_user_id": user_id,
            "patreon_user_name": user_name,
            "campaign_id": self.settings.patreon_campaign_id,
            "is_entitled": patron_status == "active_patron" and amount_cents > 0,
            "patron_status": patron_status,
            "last_charge_status": attrs.get("last_charge_status"),
            "last_charge_date": attrs.get("last_charge_date"),
            "currently_entitled_amount_cents": amount_cents,
            "tier_ids": [tier.get("id") for tier in tiers if tier.get("id")],
            "tiers": tiers,
        }

    @staticmethod
    def token_expires_at(expires_in_seconds: int) -> str:
        expires_at = datetime.now(timezone.utc) + timedelta(seconds=expires_in_seconds)
        return expires_at.isoformat()
