"""Redis-backed share token service for secure share links with device binding."""

from __future__ import annotations

import json
import secrets
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Literal

import redis.asyncio as redis

from app.core.config import get_settings


@dataclass
class ChatGrantRecord:
    chat_token: str
    tenant_id: str
    api_key: str
    session_id: str
    user_id: str | None
    bound_device_id: str
    expires_at: datetime
    created_at: datetime
    last_seen_at: datetime


@dataclass
class ClaimResult:
    ok: bool
    status: Literal["claimed", "reused", "invalid", "expired", "forbidden"]
    grant: ChatGrantRecord | None = None


class ShareTokenService:
    """Manage temporary share tokens and device-bound chat grants in Redis."""

    def __init__(self, redis_client: redis.Redis | None = None) -> None:
        self._settings = get_settings()
        self._redis = redis_client

    async def _get_client(self) -> redis.Redis:
        if self._redis is None:
            self._redis = redis.from_url(
                self._settings.redis_url,
                encoding="utf-8",
                decode_responses=True,
            )
        return self._redis

    @staticmethod
    def _token_key(token: str) -> str:
        return f"share:token:{token}"

    @staticmethod
    def _grant_key(chat_token: str) -> str:
        return f"share:grant:{chat_token}"

    async def create_token(
        self,
        tenant_id: str,
        api_key: str,
        session_id: str,
        user_id: str | None,
        expires_in_minutes: int,
    ) -> tuple[str, datetime]:
        client = await self._get_client()
        now = datetime.utcnow()
        expires_at = now + timedelta(minutes=expires_in_minutes)
        ttl_seconds = max(1, int((expires_at - now).total_seconds()))

        token = secrets.token_urlsafe(24)
        payload = {
            "tenant_id": tenant_id,
            "api_key": api_key,
            "session_id": session_id,
            "user_id": user_id,
            "expires_at": expires_at.isoformat(),
            "bound_device_id": None,
            "chat_token": None,
        }
        await client.setex(self._token_key(token), ttl_seconds, json.dumps(payload))
        return token, expires_at

    async def claim_or_reuse(self, token: str, device_id: str) -> ClaimResult:
        client = await self._get_client()
        raw = await client.get(self._token_key(token))
        if not raw:
            return ClaimResult(ok=False, status="invalid")

        data = json.loads(raw)
        expires_at = datetime.fromisoformat(data["expires_at"])
        if datetime.utcnow() > expires_at:
            await client.delete(self._token_key(token))
            return ClaimResult(ok=False, status="expired")

        if not data.get("chat_token"):
            chat_token = secrets.token_urlsafe(24)
            now = datetime.utcnow()
            ttl_seconds = max(1, int((expires_at - now).total_seconds()))

            grant_data = {
                "chat_token": chat_token,
                "tenant_id": data["tenant_id"],
                "api_key": data["api_key"],
                "session_id": data["session_id"],
                "user_id": data.get("user_id"),
                "bound_device_id": device_id,
                "expires_at": data["expires_at"],
                "created_at": now.isoformat(),
                "last_seen_at": now.isoformat(),
            }
            data["bound_device_id"] = device_id
            data["chat_token"] = chat_token

            pipe = client.pipeline()
            pipe.setex(self._grant_key(chat_token), ttl_seconds, json.dumps(grant_data))
            pipe.setex(self._token_key(token), ttl_seconds, json.dumps(data))
            await pipe.execute()

            return ClaimResult(ok=True, status="claimed", grant=self._to_grant(grant_data))

        if data.get("bound_device_id") != device_id:
            return ClaimResult(ok=False, status="forbidden")

        grant_raw = await client.get(self._grant_key(data["chat_token"]))
        if not grant_raw:
            return ClaimResult(ok=False, status="invalid")

        grant_data = json.loads(grant_raw)
        grant_data["last_seen_at"] = datetime.utcnow().isoformat()
        ttl = await client.ttl(self._grant_key(data["chat_token"]))
        if ttl and ttl > 0:
            await client.setex(self._grant_key(data["chat_token"]), ttl, json.dumps(grant_data))

        return ClaimResult(ok=True, status="reused", grant=self._to_grant(grant_data))

    async def get_chat_grant_for_device(self, chat_token: str, device_id: str) -> ChatGrantRecord | None:
        client = await self._get_client()
        raw = await client.get(self._grant_key(chat_token))
        if not raw:
            return None

        data = json.loads(raw)
        if data.get("bound_device_id") != device_id:
            return None

        expires_at = datetime.fromisoformat(data["expires_at"])
        if datetime.utcnow() > expires_at:
            await client.delete(self._grant_key(chat_token))
            return None

        data["last_seen_at"] = datetime.utcnow().isoformat()
        ttl = await client.ttl(self._grant_key(chat_token))
        if ttl and ttl > 0:
            await client.setex(self._grant_key(chat_token), ttl, json.dumps(data))
        return self._to_grant(data)

    @staticmethod
    def _to_grant(data: dict) -> ChatGrantRecord:
        return ChatGrantRecord(
            chat_token=data["chat_token"],
            tenant_id=data["tenant_id"],
            api_key=data["api_key"],
            session_id=data["session_id"],
            user_id=data.get("user_id"),
            bound_device_id=data["bound_device_id"],
            expires_at=datetime.fromisoformat(data["expires_at"]),
            created_at=datetime.fromisoformat(data["created_at"]),
            last_seen_at=datetime.fromisoformat(data["last_seen_at"]),
        )


_share_token_service: ShareTokenService | None = None


def get_share_token_service() -> ShareTokenService:
    global _share_token_service
    if _share_token_service is None:
        _share_token_service = ShareTokenService()
    return _share_token_service
