import base64
import json
import logging
import re
from dataclasses import dataclass
from re import RegexFlag
from re import compile as compile_regexp
from typing import Callable, Generic, List, TypeVar
from urllib.parse import unquote

import orjson

T = TypeVar("T")
max_length = 1024


@dataclass
class ValidationResult(Generic[T]):
    is_valid: bool
    errors: List[str] = None
    modified_result: T = None


def afdianKey(key: str, errors: List[str]) -> list[str]:
    return errors


def boostyKey(key: str, errors: List[str]) -> list[str]:
    try:
        orjson.loads(unquote(key))
    except ValueError:
        errors.append("The key is not valid JSON.")
    return errors


def discordKey(key: str, errors: List[str]) -> list[str]:
    pattern_str = r"(mfa.[a-z0-9_-]{20,})|([a-z0-9_-]{23,28}.[a-z0-9_-]{6,7}.[a-z0-9_-]{27})"
    pattern = compile_regexp(pattern_str, RegexFlag.IGNORECASE)
    key_length = len(key)

    if key_length > max_length:
        errors.append(f'The key length of "{key_length}" is over the maximum of "{max_length}".')

    if not pattern.match(key):
        errors.append(f'The key doesn\'t match the required pattern of "{pattern_str}".')

    return errors


def dlsiteKey(key: str, errors: List[str]) -> list[str]:
    key_length = len(key)

    if key_length > max_length:
        errors.append(f'The key length of "{key_length}" is over the maximum of "{max_length}".')

    return errors


def fanboxKey(key: str, errors: List[str]) -> list[str]:
    key_length = len(key)
    pattern_str = r"^\d+_\w+$"
    pattern = compile_regexp(pattern=pattern_str, flags=RegexFlag.IGNORECASE)

    if key_length > max_length:
        errors.append(f'The key length of "{key_length}" is over the maximum of "{max_length}".')

    if not pattern.match(key):
        errors.append("The key doesn't match the required pattern.")

    return errors


def fanslyKey(key: str, errors: List[str]) -> list[str]:
    key_length = len(key)
    pattern = compile_regexp(r"[a-zA-Z0-9]{71}$")

    if key_length == 71:
        if not pattern.match(key):
            errors.append("The key doesn't match the required pattern.")
    else:
        try:
            fansly_key_obj = orjson.loads(base64.b64decode(key.encode()))
        except Exception:
            errors.append("Invalid key format.")
            return errors

        if not pattern.match(fansly_key_obj.get("token", "")):
            errors.append("The key doesn't match the required pattern.")

    return errors


def fantiaKey(key: str, errors: List[str]) -> list[str]:
    req_lengths = [32, 64]
    key_length = len(key)

    if not any(list(key_length == length for length in req_lengths)):
        errors.append(
            f"The key length of \"{key_length}\" is not a valid Fantia key. "
            f"Accepted lengths: {", ".join(str(rl) for rl in req_lengths)}."
        )

    if not key.islower():
        errors.append("The key is not in lower case.")

    return errors


def gumroadKey(key: str, errors: List[str]) -> list[str]:
    min_length = 200
    key_length = len(key)

    if key_length < min_length:
        errors.append(f'The key length of "{key_length}" is less than minimum required "{min_length}".')

    if key_length > max_length:
        errors.append(f'The key length of "{key_length}" is over the maximum of "{max_length}".')

    return errors


def onlyfansKey(key: str, errors: List[str]) -> list[str]:
    key_decoded = orjson.loads(base64.b64decode(key.encode("utf8")))

    if not key_decoded["auth_id"].isdigit():
        errors.append("User ID should be only digits.")

    try:
        key_decoded["user_agent"].encode("ascii")
    except UnicodeEncodeError:
        errors.append("Invalid User Agent.")

    x_bc_pattern_str = r"[0-9a-f]+$"
    pattern = re.compile(x_bc_pattern_str)
    if not pattern.match(key_decoded["x-bc"]):
        logging.exception(f"Invalid x-bc {key_decoded["x-bc"]}")
        return "Invalid characters in x-bc."

    if len(key_decoded["x-bc"]) != 40:
        # TODO: check logs if valid ones have 40 chars only
        logging.exception(f"x-bc for only fans has length different from 40", extra=key_decoded)

    return []


def patreonKey(key: str, errors: List[str]) -> list[str]:
    req_length = 43
    key_length = len(key)

    if key_length != req_length:
        errors.append(f'The key length of "{key_length}" is not a valid Patreon key. Required length: "{req_length}".')

    if not re.match(r"[\w-]{43}", key):
        logging.exception("Invalid Patreon key", extra=dict(key=key))
        errors.append(f"The key encoding does not match, check if you copied a valid Patreon key.")

    return errors


def subscribestarKey(key: str, errors: List[str]) -> list[str]:
    key_length = len(key)

    if key_length > max_length:
        errors.append(f'The key length of "{key_length}" is over the maximum of "{max_length}".')

    return errors


def candfansKey(key: str, errors: List[str]) -> list[str]:
    try:
        parsed = set(json.loads(base64.b64decode(unquote(key))).keys()) == {"mac", "iv", "tag", "value"}
    except Exception:
        parsed = False

    if not parsed:
        errors.append(f'The key was not decodable.')

    return errors


SERVICE_CONSTRAINTS: dict[str, Callable[[str, List[str]], list[str]]] = dict(
    afdian=afdianKey,
    boosty=boostyKey,
    discord=discordKey,
    dlsite=dlsiteKey,
    fanbox=fanboxKey,
    fansly=fanslyKey,
    fantia=fantiaKey,
    gumroad=gumroadKey,
    onlyfans=onlyfansKey,
    patreon=patreonKey,
    subscribestar=subscribestarKey,
    candfans=candfansKey,
)
