import hashlib
import secrets
from datetime import datetime, timedelta, timezone
from typing import Optional

from bson.objectid import ObjectId
from MongoDBConnection import access_tokens


class AccessTokenModel:
    DEFAULT_TTL_HOURS = 12

    @staticmethod
    def _utcnow():
        return datetime.now(timezone.utc)

    @staticmethod
    def _normalize_actor_id(actor_id: str) -> str:
        return str(actor_id or "").strip()

    @staticmethod
    def hash_token(raw_token: str) -> str:
        return hashlib.sha256(str(raw_token or "").encode("utf-8")).hexdigest()

    @classmethod
    def generate_raw_token(cls) -> str:
        # 32 bytes random => đủ mạnh cho opaque token
        return secrets.token_urlsafe(32)

    @classmethod
    def issue_token(
        cls,
        actor_id: str,
        actor_role: str,
        ttl_hours: Optional[int] = None,
        created_by_ip: str = "",
        user_agent: str = "",
    ) -> dict:
        actor_id = cls._normalize_actor_id(actor_id)
        actor_role = str(actor_role or "").strip()

        if not actor_id:
            raise ValueError("actor_id is required")

        ttl = int(ttl_hours or cls.DEFAULT_TTL_HOURS)
        now = cls._utcnow()
        expires_at = now + timedelta(hours=ttl)

        raw_token = cls.generate_raw_token()
        token_hash = cls.hash_token(raw_token)

        doc = {
            "tokenHash": token_hash,
            "actorId": actor_id,
            "actorRole": actor_role,
            "expiresAt": expires_at,
            "revoked": False,
            "revokedAt": None,
            "createdAt": now,
            "lastUsedAt": now,
            "createdByIp": str(created_by_ip or "").strip(),
            "userAgent": str(user_agent or "").strip(),
        }

        insert_result = access_tokens.insert_one(doc)
        doc["_id"] = insert_result.inserted_id

        return {
            "rawToken": raw_token,
            "record": doc,
        }

    @classmethod
    def find_valid_token(cls, raw_token: str) -> Optional[dict]:
        token_hash = cls.hash_token(raw_token)
        now = cls._utcnow()

        token_doc = access_tokens.find_one({
            "tokenHash": token_hash,
            "revoked": False,
            "expiresAt": {"$gt": now},
        })

        return token_doc

    @classmethod
    def touch_token(cls, token_id):
        now = cls._utcnow()
        access_tokens.update_one(
            {"_id": token_id},
            {"$set": {"lastUsedAt": now}}
        )

    @classmethod
    def revoke_token(cls, raw_token: str) -> bool:
        token_hash = cls.hash_token(raw_token)
        result = access_tokens.update_one(
            {"tokenHash": token_hash, "revoked": False},
            {"$set": {"revoked": True, "revokedAt": cls._utcnow()}}
        )
        return result.modified_count > 0

    @classmethod
    def revoke_all_for_actor(cls, actor_id: str) -> int:
        result = access_tokens.update_many(
            {"actorId": cls._normalize_actor_id(actor_id), "revoked": False},
            {"$set": {"revoked": True, "revokedAt": cls._utcnow()}}
        )
        return result.modified_count