"""
security/http_signatures.py
Enhanced HTTP Signatures implementation.

Implements HTTP Signatures (draft-cavage-http-signatures) of request bodies.

See https://swicg.github.io/activitypub-http-signature/#how-to-verify-a-signature
"""

import base64
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
import hashlib
from logging import Logger
from urllib.parse import urlparse

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding

from phederation.utils import ObjectId
from phederation.utils.exceptions import (
    SignatureError,
    catch_exceptions,
)
from phederation.utils.logging import configure_logger

from .key_management import KeyManager, KeyPair


@dataclass
class HeaderSignature:
    algorithm: str
    key_id: str
    signed_headers: list[str]
    signature: bytes

    def build(self) -> str:
        return (
            f'keyId="{self.key_id}",'
            f"algorithm={self.algorithm},"
            f'headers="{" ".join(self.signed_headers)}",'
            f'signature="{base64.b64encode(self.signature).decode(encoding='utf-8')}"'
        )

    @catch_exceptions(SignatureError, "Invalid header signature")
    @staticmethod
    async def parse(header: str) -> "HeaderSignature":
        """Parse HTTP signature header."""
        parts: dict[str, str] = {}
        for part in header.split(","):
            if "=" not in part:
                continue
            key, value = part.split("=", 1)
            parts[key.strip()] = value.strip('"')

        required = ["keyId", "algorithm", "headers", "signature"]
        if not all(k in parts for k in required):
            raise SignatureError(f"Missing required signature parameters (is {header}, should be {required})")

        signature_bytes = base64.b64decode(parts["signature"])
        return HeaderSignature(
            algorithm=parts["algorithm"], key_id=parts["keyId"], signed_headers=parts["headers"].split(), signature=signature_bytes
        )


class HTTPSignatureVerifier:
    """HTTP signature verification.

    See https://swicg.github.io/activitypub-http-signature/#how-to-verify-a-signature
    """

    def __init__(self, key_manager: KeyManager):
        """Initialize signature verifier."""
        self.key_manager: KeyManager = key_manager
        self.logger: Logger = configure_logger(__name__, prefix=key_manager.storage.settings.federation.logging_prefix)

    @catch_exceptions(SignatureError, "Could not verify request signature")
    async def verify_signature(
        self, headers: dict[str, str], method: str, path: str, verified_key_id: None | str, body_serialized: None | str
    ) -> ObjectId | None:
        """Verify HTTP signature on request."""
        headers = {key.lower(): headers[key] for key in headers.keys()}

        # then verify signature
        if "signature" not in headers:
            self.logger.debug("No 'signature' field found in headers")
            return None

        # verify TTL first
        if "date" not in headers:
            self.logger.warning("No 'date' field found in headers")
            return None

        if not self._verify_date(headers["date"]):
            self.logger.warning(f"Http signature expired, TTL is {self.key_manager.settings.security.signature_ttl} seconds")
            return None

        sig_parts = await HeaderSignature.parse(header=headers["signature"])

        # Verify signing algorithm
        allowed_algorithms = self.key_manager.settings.security.allowed_algorithms
        if sig_parts.algorithm not in allowed_algorithms:
            self.logger.warning(f"Algorithm '{sig_parts.algorithm}' not in allowed algorithms ({allowed_algorithms})")
            return None

        # Get signing key
        if verified_key_id and verified_key_id != sig_parts.key_id:
            self.logger.warning(f"keyId in signature is not equal to verified_key_id ('{sig_parts.key_id}' is not '{verified_key_id}')")
            return None

        # If body is provided and digest header exists, verify the digest
        if self.key_manager.settings.security.require_body_digest:
            if body_serialized is None or "digest" not in headers:
                self.logger.warning("Body digest verification failed")
                return None
            expected_digest = self._generate_digest(body_serialized=body_serialized)
            if headers["digest"] != expected_digest:
                self.logger.warning("Body digest verification failed")
                return None

        signing_string = self._build_signing_string(method=method, path=path, signed_headers=sig_parts.signed_headers, headers=headers)

        # Get public key, possibly resolving it from the network
        public_key = await self.key_manager.get_public_key(key_id=sig_parts.key_id)
        if not public_key:
            self.logger.error(f"Failed to get public key for {sig_parts.key_id}")
            return None

        # Verify signature
        try:
            public_key.verify(sig_parts.signature, signing_string.encode(encoding="utf-8"), padding.PKCS1v15(), hashes.SHA256())
            return sig_parts.key_id
        except InvalidSignature as e:
            self.logger.warning(f"Invalid signature, error: '{e}', sig_parts '{sig_parts}")
            return None

    @catch_exceptions(SignatureError, "sign request failed")
    async def sign_request(
        self, method: str, path: str, headers: dict[str, str], key_id: ObjectId, body_serialized: None | str = None
    ) -> dict[str, str]:
        """Sign HTTP request."""
        # make sure to remove the host
        path = urlparse(path).path

        request_headers = headers.copy()

        # Add date if not present
        if "date" not in request_headers:
            now = datetime.now(timezone.utc)
            request_headers["date"] = now.strftime("%a, %d %b %Y %H:%M:%S GMT")

        # Calculate digest first
        if body_serialized is not None:
            digest = self._generate_digest(body_serialized)
            request_headers["digest"] = digest
            self.logger.debug(f"Added digest header: {digest}")

        # Headers to sign (in specific order)
        headers_to_sign = ["(request-target)", "host", "date", "digest"]

        # Build signing string
        signing_string = self._build_signing_string(method, path, request_headers, headers_to_sign)

        self.logger.debug(f"Headers being signed: {headers_to_sign}. Signing string: {signing_string}")

        # Get private key
        if self.key_manager:
            active_key: KeyPair = await self.key_manager.get_RSA_key_pair(key_id=key_id)
        else:
            raise SignatureError("No key manager available")
        if not active_key.private_key:
            raise SignatureError(f"HTTPSignatureVerifier: Key id '{key_id}' does not have a local private key.")

        # Sign
        signature = active_key.private_key.sign(signing_string.encode(encoding="utf-8"), padding.PKCS1v15(), hashes.SHA256())

        # Build signature header
        signature_header = HeaderSignature(
            key_id=active_key.key_id, algorithm='"rsa-sha256"', signed_headers=headers_to_sign, signature=signature
        ).build()

        # Return headers with both Digest and Signature
        signed_headers = {**request_headers, "Signature": signature_header}

        self.logger.debug(f"Final headers: {signed_headers}")
        return signed_headers

    def _verify_date(self, date_header: None | str) -> bool:
        """
        Verify request date with clock skew handling.

        Allows 5 minutes of clock skew in either direction.
        """
        if not date_header:
            return False

        try:
            request_time = datetime.strptime(date_header, "%a, %d %b %Y %H:%M:%S GMT").replace(tzinfo=None)

            # Use test time if set, otherwise use current time
            now = datetime.now(timezone.utc)
            now = now.replace(tzinfo=None)

            skew = timedelta(minutes=5)
            earliest = now - skew
            latest = now + skew

            self.logger.debug(f"Date verification: request_time={request_time}, now={now}, earliest={earliest}, latest={latest}")

            return earliest <= request_time <= latest

        except Exception as e:
            self.logger.debug(f"Date verification failed: {e}")
            return False

    def _build_signing_string(self, method: str, path: str, headers: dict[str, str], signed_headers: list[str]) -> str:
        """Build string to sign."""
        lines: list[str] = []
        # Convert headers to case-insensitive dictionary
        headers_lower = {k.lower(): v for k, v in headers.items()}

        for header in signed_headers:
            if header == "(request-target)":
                lines.append(f"(request-target): {method.lower()} {path}")
            else:
                header_lower = header.lower()
                if header_lower not in headers_lower:
                    self.logger.error(f"Missing required header: {header}. Available headers: {list(headers_lower.keys())}")
                    raise SignatureError(f"Missing required header: {header}")
                lines.append(f"{header_lower}: {headers_lower[header_lower]}")

        signing_string = "\n".join(lines)
        self.logger.debug(f"Signing string: {signing_string}")
        return signing_string

    def _generate_digest(self, body_serialized: str) -> str:
        """
        Generate digest header value for request body.

        Args:
            body_serialized: Request body string (already serialized)

        Returns:
            Digest header value
        """
        body_bytes = body_serialized.encode("utf-8")

        # Calculate SHA-256 digest
        digest = hashlib.sha256(body_bytes).digest()
        digest_b64 = base64.b64encode(digest).decode("ascii")

        return f"SHA-256={digest_b64}"
