"""Join the P2HS Discord server and assign a plan-based community role."""
from __future__ import annotations

from datetime import datetime, timedelta, timezone
from typing import Optional
from uuid import UUID

from jose import JWTError, jwt

from src.config import Settings, get_settings
from src.core.exceptions import ConflictError, ValidationError
from src.domain.auth.repository import UserRepository
from src.domain.billing.normalization import is_paid_billing_status, is_payable_plan, is_payable_plan
from src.domain.billing.repository import SubscriptionRepository
from src.domain.discord_community.client import DiscordCommunityClient, DiscordHttpClient
from src.domain.discord_community.roles import MANAGED_ROLE_NAMES, community_role_name
from src.domain.workspaces.repository import WorkspaceRepository

_STATE_ALG = "HS256"
_STATE_TTL_MIN = 20


def encode_discord_oauth_state(user_id: UUID, settings: Optional[Settings] = None) -> str:
    cfg = settings or get_settings()
    now = datetime.now(timezone.utc)
    return jwt.encode(
        {
            "sub": str(user_id),
            "aud": "discord-community",
            "iat": int(now.timestamp()),
            "exp": int((now + timedelta(minutes=_STATE_TTL_MIN)).timestamp()),
        },
        cfg.jwt_secret,
        algorithm=_STATE_ALG,
    )


def decode_discord_oauth_state(token: str, settings: Optional[Settings] = None) -> UUID:
    cfg = settings or get_settings()
    try:
        payload = jwt.decode(
            token,
            cfg.jwt_secret,
            algorithms=[_STATE_ALG],
            audience="discord-community",
        )
    except JWTError as exc:
        raise ValidationError("Invalid or expired Discord state token") from exc
    sub = payload.get("sub")
    if not sub:
        raise ValidationError("Invalid Discord state payload")
    return UUID(str(sub))


class DiscordCommunityService:
    def __init__(
        self,
        users: UserRepository,
        workspaces: WorkspaceRepository,
        subscriptions: SubscriptionRepository,
        client: Optional[DiscordCommunityClient] = None,
        settings: Optional[Settings] = None,
    ):
        self._users = users
        self._workspaces = workspaces
        self._subscriptions = subscriptions
        self._settings = settings or get_settings()
        self._client = client or DiscordHttpClient(self._settings)

    def _configured(self) -> bool:
        s = self._settings
        return all(
            [
                (s.discord_client_id or "").strip(),
                (s.discord_client_secret or "").strip(),
                (s.discord_bot_token or "").strip(),
                (s.discord_guild_id or "").strip(),
                (s.discord_oauth_redirect_uri or "").strip(),
            ]
        )

    def redirect_uri(self) -> str:
        return (self._settings.discord_oauth_redirect_uri or "").strip()

    def build_connect_url(self, user_id: UUID) -> tuple[str, str]:
        if not self._configured():
            raise ValidationError("Discord community is not configured.")
        state = encode_discord_oauth_state(user_id, self._settings)
        url = self._client.authorization_url(redirect_uri=self.redirect_uri(), state=state)
        return url, state

    async def resolve_role_name(self, user_id: UUID) -> str:
        workspaces = await self._workspaces.list_for_user(user_id)
        plan_slugs: list[Optional[str]] = []
        any_paid = False
        for workspace in workspaces:
            if getattr(workspace, "deleted_at", None):
                continue
            sub = await self._subscriptions.get_latest_for_workspace(workspace.id)
            slug = (getattr(sub, "plan_slug", None) if sub else None) or getattr(
                workspace, "selected_plan_slug", None
            )
            plan_slugs.append(slug)
            if (
                sub
                and is_paid_billing_status(getattr(sub, "status", None) or "")
                and is_payable_plan(slug)
            ):
                any_paid = True
        return community_role_name(plan_slugs=plan_slugs, any_paid=any_paid)

    async def _ensure_role_id(self, role_name: str) -> str:
        roles = await self._client.list_guild_roles()
        for role in roles:
            if str(role.get("name") or "") == role_name:
                return str(role["id"])
        created = await self._client.create_guild_role(role_name)
        return str(created["id"])

    async def complete_oauth(self, *, user_id: UUID, code: str, state: str) -> dict:
        if decode_discord_oauth_state(state, self._settings) != user_id:
            raise ValidationError("Discord state does not match the signed-in user.")
        token = await self._client.exchange_code(code=code, redirect_uri=self.redirect_uri())
        access_token = str(token["access_token"])
        profile = await self._client.get_current_user(access_token)
        discord_user_id = str(profile.get("id") or "")
        if not discord_user_id:
            raise ValidationError("Discord did not return a user id.")
        username = str(profile.get("username") or "") or None
        existing = await self._users.get_by_discord_user_id(discord_user_id)
        if existing and existing.id != user_id:
            raise ConflictError("That Discord account is already linked to another P2HS user.")
        role_name = await self.resolve_role_name(user_id)
        role_id = await self._ensure_role_id(role_name)
        await self._client.add_guild_member(
            discord_user_id=discord_user_id,
            access_token=access_token,
            role_ids=[role_id],
        )
        await self._client.add_guild_member_role(
            discord_user_id=discord_user_id,
            role_id=role_id,
        )
        user = await self._users.set_discord_connection(
            user_id,
            discord_user_id=discord_user_id,
            discord_username=username,
        )
        if not user:
            raise ValidationError("User not found.")
        return {
            "connected": True,
            "discord_user_id": discord_user_id,
            "discord_username": username,
            "role_name": role_name,
        }

    @staticmethod
    def managed_role_names() -> tuple[str, ...]:
        return MANAGED_ROLE_NAMES
