import copy
from typing import Any, Callable
from .model import (
    HeaderMember,
    GeneralJSONSignature,
    FlattenedJSONSignature,
)
from .types import (
    JSONSignatureDict,
    GeneralJSONSerialization,
    FlattenedJSONSerialization,
)
from .registry import JWSRegistry
from ..registry import reject_unprotected_crit_header
from ..util import (
    to_bytes,
    json_b64encode,
    json_b64decode,
    urlsafe_b64encode,
    urlsafe_b64decode,
)
from ..errors import DecodeError

__all__ = [
    "FindKey",
    "sign_general_json",
    "sign_flattened_json",
    "sign_json_member",
    "extract_general_json",
    "verify_general_json",
    "verify_flattened_json",
    "detach_json_content",
]

FindKey = Callable[[HeaderMember], Any]


def sign_general_json(
    members: list[HeaderMember],
    payload: bytes,
    registry: JWSRegistry,
    find_key: FindKey,
) -> GeneralJSONSerialization:
    payload_segment = urlsafe_b64encode(payload)
    signatures: list[JSONSignatureDict] = [
        sign_json_member(payload_segment, member, registry, find_key) for member in members
    ]
    return {
        "payload": payload_segment.decode("utf-8"),
        "signatures": signatures,
    }


def sign_flattened_json(
    member: HeaderMember,
    payload: bytes,
    registry: JWSRegistry,
    find_key: FindKey,
) -> FlattenedJSONSerialization:
    payload_segment = urlsafe_b64encode(payload)
    signature = sign_json_member(payload_segment, member, registry, find_key)
    data: FlattenedJSONSerialization = {"payload": payload_segment.decode("utf-8"), **signature}
    return data


def sign_json_member(
    payload_segment: bytes, member: HeaderMember, registry: JWSRegistry, find_key: FindKey
) -> JSONSignatureDict:
    reject_unprotected_crit_header(member.header)
    headers = member.headers()
    registry.check_header(headers)
    alg = registry.get_alg(headers["alg"])
    key = find_key(member)
    alg.check_key(key)
    if member.protected:
        protected_segment = json_b64encode(member.protected)
    else:
        protected_segment = b""
    signing_input = b".".join([protected_segment, payload_segment])
    signature = urlsafe_b64encode(alg.sign(signing_input, key))
    rv: JSONSignatureDict = {"signature": signature.decode("utf-8")}
    if member.protected:
        rv["protected"] = protected_segment.decode("utf-8")
    if member.header:
        rv["header"] = member.header
    return rv


def extract_general_json(value: GeneralJSONSerialization, registry: JWSRegistry) -> GeneralJSONSignature:
    payload_segment: bytes = value["payload"].encode("utf-8")
    registry.validate_payload_size(payload_segment)
    try:
        payload = urlsafe_b64decode(payload_segment)
    except (TypeError, ValueError):
        raise DecodeError("Invalid payload")

    signatures: list[JSONSignatureDict] = value["signatures"]
    members = [__signature_to_member(sig, registry) for sig in signatures]
    obj = GeneralJSONSignature(members, payload)
    obj.signatures = signatures
    obj.segments = {"payload": payload_segment}
    return obj


def __signature_to_member(sig: JSONSignatureDict, registry: JWSRegistry) -> HeaderMember:
    member = HeaderMember()
    if "protected" in sig:
        protected_segment = to_bytes(sig["protected"])
        registry.validate_header_size(protected_segment)
        member.protected = json_b64decode(protected_segment)
    if "header" in sig:
        member.header = sig["header"]
    return member


def verify_general_json(obj: GeneralJSONSignature, registry: JWSRegistry, find_key: FindKey) -> bool:
    payload_segment = obj.segments["payload"]
    for index, signature in enumerate(obj.signatures):
        member = obj.members[index]
        if not verify_signature(member, signature, payload_segment, registry, find_key):
            return False
    return True


def verify_flattened_json(obj: FlattenedJSONSignature, registry: JWSRegistry, find_key: FindKey) -> bool:
    payload_segment = obj.segments["payload"]
    assert obj.signature is not None
    return verify_signature(obj.member, obj.signature, payload_segment, registry, find_key)


def verify_signature(
    member: HeaderMember,
    signature: JSONSignatureDict,
    payload_segment: bytes,
    registry: JWSRegistry,
    find_key: FindKey,
) -> bool:
    reject_unprotected_crit_header(member.header)
    headers = member.headers()
    registry.check_header(headers)
    alg = registry.get_alg(headers["alg"])
    key = find_key(member)
    alg.check_key(key)

    if "protected" in signature:
        protected_segment = to_bytes(signature["protected"])
    else:
        protected_segment = b""

    signature_segment = to_bytes(signature["signature"])
    registry.validate_signature_size(signature_segment)

    sig = urlsafe_b64decode(signature_segment)
    signing_input = b".".join([protected_segment, payload_segment])
    return alg.verify(signing_input, sig, key)


def detach_json_content(value: dict[str, Any]) -> dict[str, Any]:
    # https://www.rfc-editor.org/rfc/rfc7515#appendix-F
    rv = copy.deepcopy(value)  # don't alter original value
    if "payload" in rv:
        del rv["payload"]
    return rv
