"""
OnlyFans Request Signing Module

Handles the dynamic request signing required by the OnlyFans API.
Fetches signing rules from the DATAHOARDERS/dynamic-rules GitHub repo
and computes SHA-1 based signatures for each API request.

Isolated module so it's easy to update when OF changes their signing scheme.
"""

import hashlib
import time
from typing import Dict, Optional

import aiohttp

RULES_URL = "https://raw.githubusercontent.com/DATAHOARDERS/dynamic-rules/main/onlyfans.json"


class OnlyFansSigner:
    """
    Computes request signatures for the OnlyFans API.

    Uses dynamic rules fetched from a public GitHub repo (same source as OF-Scraper).
    Rules are cached locally and refreshed every 6 hours.
    """

    RULES_TTL = 6 * 3600  # 6 hours

    def __init__(self, rules_url: Optional[str] = None):
        self.rules_url = rules_url or RULES_URL
        self._rules: Optional[Dict] = None
        self._rules_fetched_at: float = 0

    @property
    def rules_stale(self) -> bool:
        """Check if cached rules need refreshing"""
        if self._rules is None:
            return True
        return (time.time() - self._rules_fetched_at) > self.RULES_TTL

    async def get_rules(self) -> Dict:
        """
        Fetch signing rules, using cache if fresh.

        Returns:
            Dict with keys: static_param, format, checksum_indexes,
            checksum_constants, checksum_constant, app_token
        """
        if not self.rules_stale:
            return self._rules

        timeout = aiohttp.ClientTimeout(total=15)
        async with aiohttp.ClientSession(timeout=timeout) as session:
            async with session.get(self.rules_url) as resp:
                if resp.status != 200:
                    if self._rules is not None:
                        # Use stale cache rather than failing
                        return self._rules
                    raise RuntimeError(
                        f"Failed to fetch OF signing rules: HTTP {resp.status}"
                    )
                self._rules = await resp.json(content_type=None)
                self._rules_fetched_at = time.time()

        return self._rules

    async def sign(self, endpoint_path: str, user_id: str = "0") -> Dict[str, str]:
        """
        Compute signing headers for an OnlyFans API request.

        Args:
            endpoint_path: The full URL path (e.g. "/api2/v2/users/me")
            user_id: The authenticated user's ID (from auth_id cookie)

        Returns:
            Dict with 'sign', 'time', 'app-token' headers
        """
        rules = await self.get_rules()
        # Timestamp in milliseconds (matching OF-Scraper's implementation)
        timestamp = str(round(time.time() * 1000))

        # 1. Build the message to hash
        msg = "\n".join([
            rules["static_param"],
            timestamp,
            endpoint_path,
            str(user_id),
        ])

        # 2. SHA-1 hash
        sha1_hash = hashlib.sha1(msg.encode("utf-8")).hexdigest()
        sha1_bytes = sha1_hash.encode("ascii")

        # 3. Checksum from indexed byte positions + single constant
        # (matching OF-Scraper's implementation)
        checksum_indexes = rules["checksum_indexes"]
        checksum_constant = rules.get("checksum_constant", 0)
        checksum = sum(sha1_bytes[i] for i in checksum_indexes) + checksum_constant

        # 4. Build the sign header using the format template
        # Typical format: "53760:{}:{:x}:69723085"
        sign_value = rules["format"].format(sha1_hash, abs(checksum))

        return {
            "sign": sign_value,
            "time": timestamp,
            "app-token": rules["app_token"],
        }
