import urllib
from typing import Optional

from formsg.exceptions import MissingSecretKeyException, WebhookAuthenticateException
from formsg.util.parser import parse_signature_header
from formsg.util.webhook import has_epoch_expired, is_signature_valid, sign


class Webhook(object):
    def __init__(self, public_key: str, secret_key: Optional[str] = None):
        self.public_key = public_key
        self.secret_key = secret_key

    def authenticate(self, header: str, uri: str) -> bool:
        """
        Injects the webhook public key for authentication
        :param header: X-FormSG-Signature header
        :param uri: The endpoint that FormSG is POSTing to
        :rtype: :class:`bool` true if the header is verified
        :raises WebhookAuthenticateException: If the signature or uri cannot be verified
        """
        signature_header = parse_signature_header(header)
        [signature, epoch, submission_id, form_id] = [
            signature_header["v1"],
            signature_header["t"],
            signature_header["s"],
            signature_header["f"],
        ]

        # verify signature authenticity
        if not is_signature_valid(uri, signature_header, self.public_key):
            raise WebhookAuthenticateException(
                f"Signature could not be verified for uri={uri} submission_id={submission_id} form_id={form_id} epoch={epoch} signature={signature}"
            )

        # verify epoch recency
        if has_epoch_expired(epoch):
            raise WebhookAuthenticateException(
                "Signature is not recent for uri={uri} submission_id={submission_id} form_id={form_id} epoch={epoch} signature={signature}"
            )

        return True

    def generate_signature(self, params) -> str:
        """
        Generates a signature based on the URI, submission ID and epoch timestamp.
        :param params The parameters needed to generate the signature
        :param params.uri Full URL of the request
        :param params.submissionId Submission Mongo ObjectId saved to the database
        :param params.epoch Number of milliseconds since Jan 1, 1970
        :returns the generated signature
        :raises MissingSecretKeyException if a secret key is not provided when instantiating this class
        :raises TypeError if any parameters are undefined
        """
        if not self.secret_key:
            raise MissingSecretKeyException()

        [submission_id, uri, form_id, epoch] = [
            params["submissionId"],
            params["uri"],
            params["formId"],
            params["epoch"],
        ]
        if not submission_id or not uri or not form_id or not epoch:
            raise TypeError(
                "submissionId, uri, formId, or epoch must be provided to generate a webhook signature"
            )

        parsed_url = urllib.parse.urlparse(uri).geturl()
        base_string = f"{parsed_url}.{submission_id}.{form_id}.{epoch}"
        return sign(base_string, self.secret_key).decode("utf-8")

    def construct_header(self, params) -> str:
        """
        Constructs the `X-FormSG-Signature` header
        :param params The parameters needed to construct the header
        :param params.epoch Epoch timestamp
        :param params.submissionId Mongo ObjectId
        :param params.formId Mongo ObjectId
        :param params.signature A signature generated by the generate_signature() function
        :rtype str: The `X-FormSG-Signature` header
        :raises MissingSecretKeyException if a secret key is not provided when instantiating this class
        """
        [epoch, submission_id, form_id, signature] = [
            params["epoch"],
            params["submissionId"],
            params["formId"],
            params["signature"],
        ]
        if not self.secret_key:
            raise MissingSecretKeyException()
        return f"t={epoch},s={submission_id},f={form_id},v1={signature}"
