import base64
from datetime import datetime, timezone
import hashlib
from typing import Literal

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
    Ed25519PrivateKey,
    Ed25519PublicKey,
)
from pydantic import BaseModel, Field

from phederation.utils.base import APDateTime, ActivityPubBaseWithId, ObjectId
from phederation.utils.exceptions import SecurityError


class TokenRequest(BaseModel):
    username: str
    password: str


class TokenResponse(BaseModel):
    access_token: str
    token_type: str = "bearer"


def to_sha256(argument: str | bytes):
    """Computes the hexdigest of the argument using SHA256.

    Args:
        string_to_hash (str | bytes): String or bytes array to hash. The string will be encoded using utf-8.

    Returns:
        str: SHA256 of the encoded string.
    """
    if isinstance(argument, str):
        argument = argument.encode(encoding="utf-8")

    hash = hashlib.sha256()
    hash.update(argument)
    hash_sha256 = hash.hexdigest()
    return hash_sha256


def sign_rsa(string_to_sign: str, private_key: rsa.RSAPrivateKey):
    signature = private_key.sign(string_to_sign.encode(encoding="utf-8"), padding.PKCS1v15(), hashes.SHA256())
    return base64.b64encode(signature).decode(encoding="utf-8")


def verify_rsa(signature_string: str, string_to_verify: str, public_key: rsa.RSAPublicKey):
    signature = base64.b64decode(signature_string)
    try:
        _ = public_key.verify(
            signature=signature, data=string_to_verify.encode(encoding="utf-8"), padding=padding.PKCS1v15(), algorithm=hashes.SHA256()
        )
    except InvalidSignature:
        return False
    return True


def sign_ed25519(string_to_sign: str, private_key: Ed25519PrivateKey):
    signature = private_key.sign(string_to_sign.encode(encoding="utf-8"))
    return base64.b64encode(signature).decode(encoding="utf-8")


def verify_ed25519(signature_string: str, string_to_verify: str, public_key: Ed25519PublicKey):
    signature = base64.b64decode(signature_string)
    try:
        public_key.verify(signature, string_to_verify.encode(encoding="utf-8"))
    except InvalidSignature:
        return False
    return True


class DataIntegrityProof(ActivityPubBaseWithId):
    """A data integrity proof provides information about the proof mechanism,
    parameters required to verify that proof, and the proof value itself.
    When expressing a data integrity proof on an object, a "proof" property of type DataIntegrityProof MUST be used.

    https://w3c.github.io/vc-data-integrity/#dfn-data-integrity-proof
    """

    type: Literal["DataIntegrityProof"] = Field(
        default="DataIntegrityProof",
        description="Always set to DataIntegrityProof here, the cryptographySuite field specifies which type of proof.",
    )

    proof_purpose: Literal["assertionMethod"] | Literal["authentication"] = Field(
        default="assertionMethod",
        description="The reason the proof was created MUST be specified as a string that maps to a URL. The proof purpose acts as a safeguard to prevent the proof from being misused by being applied to a purpose other than the one that was intended. For example, without this value the creator of a proof could be tricked into using cryptographic material typically used to create a Verifiable Credential (assertionMethod) during a login process (authentication) which would then result in the creation of a verifiable credential they never meant to create instead of the intended action, which was to merely log in to a website.",
    )

    description: str | None = Field(
        default=None, description="Optional, human readable string to describe what the proof is doing or why it was issued."
    )

    verification_method: ObjectId | None = Field(
        default=None,
        description="A verification method is the means and information needed to verify the proof. If included, the value MUST be a string that maps to a [URL]. Inclusion of verificationMethod is OPTIONAL, but if it is not included, other properties such as cryptosuite might provide a mechanism by which to obtain the information necessary to verify the proof. Note that when verificationMethod is expressed in a data integrity proof, the value points to the actual location of the data; that is, the verificationMethod references, via a URL, the location of the public key that can be used to verify the proof. This public key data is stored in a controlled identifier document, which contains a full description of the verification method.",
    )

    cryptosuite: Literal["RSA"] | Literal["Ed25519Signature2020"] | None = Field(
        default=None,
        description="An identifier for the cryptographic suite that can be used to verify the proof. If the proof type is DataIntegrityProof, cryptosuite MUST be specified (RSA or Ed25519Signature2020); otherwise, cryptosuite MAY be specified. If specified, its value MUST be a string.",
    )

    created: APDateTime | None = Field(
        default=None,
        description="The date and time the proof was created is OPTIONAL.",
    )

    expires: APDateTime | None = Field(default=None, description="The expires property is OPTIONAL and, if present, specifies when the proof expires.")

    domain: str | None = Field(
        default=None,
        description="The domain property is OPTIONAL. It conveys one or more security domains in which the proof is meant to be used. If specified, the associated value MUST be either a string, or an unordered set of strings. A verifier SHOULD use the value to ensure that the proof was intended to be used in the security domain in which the verifier is operating. The specification of the domain parameter is useful in challenge-response protocols where the verifier is operating from within a security domain known to the creator of the proof.",
        examples=["domain.example", "https://domain.example:8443", "mycorp-intranet", "b31d37d4-dd59-47d3-9dd8-c973da43b63a"],
    )

    challenge: str | None = Field(
        default=None,
        description="A string value that SHOULD be included in a proof if a domain is specified. The value is used once for a particular domain and window of time. This value is used to mitigate replay attacks.",
        examples=["1235abcd6789", "79d34551-ae81-44ae-823b-6dadbab9ebd4", "ruby"],
    )

    proof_value: str | None = Field(
        default=None,
        description="A string value that expresses base-encoded binary data necessary to verify the digital proof using the verificationMethod specified. The value MUST use a header and encoding to express the binary data. The contents of this value are determined by a specific cryptosuite and set to the proof value generated by the 'Add Proof Algorithm' for that cryptosuite. Alternative properties with different encodings specified by the cryptosuite MAY be used, instead of this property, to encode the data necessary to verify the digital proof.",
    )

    previous_proof: str | list[str] | None = Field(
        default=None,
        description="The previousProof property is OPTIONAL. If present, it MUST be a string value or an unordered list of string values. Each value identifies another data integrity proof, all of which MUST also verify for the current proof to be considered verified.",
    )

    nonce: str | None = Field(
        default=None,
        description="An OPTIONAL string value supplied by the proof creator. One use of this field is to increase privacy by decreasing linkability that is the result of deterministically generated signatures.",
    )

    def sign(self, string_to_sign: str, key: rsa.RSAPrivateKey | Ed25519PrivateKey, public_key_id: ObjectId | None = None):
        if self.cryptosuite == "RSA" and isinstance(key, rsa.RSAPrivateKey):
            proofValue = sign_rsa(string_to_sign=string_to_sign, private_key=key)
        elif self.cryptosuite == "Ed25519Signature2020" and isinstance(key, Ed25519PrivateKey):
            proofValue = sign_ed25519(string_to_sign=string_to_sign, private_key=key)
        else:
            raise SecurityError(f"DataIntegrityProof signing: type {self.type} does not match the key type {type(key)}.")
        self.proof_value = proofValue
        self.created = datetime.now(tz=timezone.utc)
        if public_key_id:
            self.verification_method = public_key_id
        return self

    def verify(self, string_to_verify: str, key: rsa.RSAPublicKey | Ed25519PublicKey) -> bool:
        if self.proof_value and self.cryptosuite == "RSA" and isinstance(key, rsa.RSAPublicKey):
            return verify_rsa(signature_string=self.proof_value, string_to_verify=string_to_verify, public_key=key)
        elif self.proof_value and self.cryptosuite == "Ed25519Signature2020" and isinstance(key, Ed25519PublicKey):
            return verify_ed25519(signature_string=self.proof_value, string_to_verify=string_to_verify, public_key=key)
        raise SecurityError(f"DataIntegrityProof verification: type {self.type} does not match the key type {type(key)}.")
