import base64
import logging
from datetime import datetime, timezone, timedelta
from typing import List, TypedDict, Optional

from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey, RSAPrivateKey
from cryptography.x509.oid import ExtensionOID
from flask import Blueprint, request, jsonify, abort
from flask.typing import ResponseReturnValue
from sqlalchemy import exc
from tldextract import tldextract

from app.api.util import ListFilter, MAX_DOMAIN_NAME_LENGTH, DOMAIN_NAME_REGEX, list_resources, MAX_ALLOWED_ITEMS
from app.util.x509 import build_certificate_chain, validate_certificate_chain, load_certificates_from_pem
from app.extensions import db
from app.models.base import Group
from app.models.mirrors import Origin, Proxy
from app.models.onions import Onion
from app.util.onion import onion_hostname

api_web = Blueprint('web', __name__)


@api_web.route('/group', methods=['GET'])
def list_groups() -> ResponseReturnValue:
    return list_resources(
        Group,
        lambda group: group.to_dict(),
        resource_name='OriginGroupList',
        max_allowed_items=MAX_ALLOWED_ITEMS,
        protective_marking='amber',
    )


@api_web.route('/origin', methods=['GET'])
def list_origins() -> ResponseReturnValue:
    domain_name_filter = request.args.get('DomainName')
    group_id_filter = request.args.get('GroupId')

    filters: List[ListFilter] = []

    if domain_name_filter:
        if len(domain_name_filter) > MAX_DOMAIN_NAME_LENGTH:
            abort(400, description=f"DomainName cannot exceed {MAX_DOMAIN_NAME_LENGTH} characters.")
        if not DOMAIN_NAME_REGEX.match(domain_name_filter):
            abort(400, description="DomainName contains invalid characters.")
        filters.append(Origin.domain_name.ilike(f"%{domain_name_filter}%"))

    if group_id_filter:
        try:
            filters.append(Origin.group_id == int(group_id_filter))
        except ValueError:
            abort(400, description="GroupId must be a valid integer.")

    return list_resources(
        Origin,
        lambda origin: origin.to_dict(),
        filters=filters,
        resource_name='OriginsList',
        max_allowed_items=MAX_ALLOWED_ITEMS,
        protective_marking='amber',
    )


@api_web.route('/mirror', methods=['GET'])
def list_mirrors() -> ResponseReturnValue:
    filters = []

    twenty_four_hours_ago = datetime.now(timezone.utc) - timedelta(hours=24)
    status_filter = request.args.get('Status')
    if status_filter:
        if status_filter == "pending":
            filters.append(Proxy.url.is_(None))
            filters.append(Proxy.deprecated.is_(None))
            filters.append(Proxy.destroyed.is_(None))
        if status_filter == "active":
            filters.append(Proxy.url.is_not(None))
            filters.append(Proxy.deprecated.is_(None))
            filters.append(Proxy.destroyed.is_(None))
        if status_filter == "expiring":
            filters.append(Proxy.deprecated.is_not(None))
            filters.append(Proxy.destroyed.is_(None))
        if status_filter == "destroyed":
            filters.append(Proxy.destroyed > twenty_four_hours_ago)
    else:
        filters.append((Proxy.destroyed.is_(None)) | (Proxy.destroyed > twenty_four_hours_ago))

    return list_resources(
        Proxy,
        lambda proxy: proxy.to_dict(),
        filters=filters,
        resource_name='MirrorsList',
        max_allowed_items=MAX_ALLOWED_ITEMS,
        protective_marking='amber',
    )


@api_web.route('/onion', methods=['GET'])
def list_onions() -> ResponseReturnValue:
    domain_name_filter = request.args.get('DomainName')
    group_id_filter = request.args.get('GroupId')

    filters: List[ListFilter] = [
        (Onion.destroyed.is_(None))
    ]

    if domain_name_filter:
        if len(domain_name_filter) > MAX_DOMAIN_NAME_LENGTH:
            abort(400, description=f"DomainName cannot exceed {MAX_DOMAIN_NAME_LENGTH} characters.")
        if not DOMAIN_NAME_REGEX.match(domain_name_filter):
            abort(400, description="DomainName contains invalid characters.")
        filters.append(Onion.domain_name.ilike(f"%{domain_name_filter}%"))

    if group_id_filter:
        try:
            filters.append(Onion.group_id == int(group_id_filter))
        except ValueError:
            abort(400, description="GroupId must be a valid integer.")

    return list_resources(
        Onion,
        lambda onion: onion.to_dict(),
        filters=filters,
        resource_name='OnionsList',
        max_allowed_items=MAX_ALLOWED_ITEMS,
        protective_marking='amber',
    )


class CreateOnionRequest(TypedDict):
    DomainName: str
    Description: str
    GroupId: int
    OnionPrivateKey: str
    OnionPublicKey: str
    TlsPrivateKey: str
    TlsCertificate: str
    SkipChainValidation: Optional[bool]
    SkipNameValidation: Optional[bool]


@api_web.route("/onion", methods=["POST"])
def create_onion() -> ResponseReturnValue:
    data: Optional[CreateOnionRequest] = request.json
    if not data:
        abort(400)

    errors = []

    required_fields = ["DomainName", "Description", "OnionPrivateKey", "OnionPublicKey", "GroupId", "TlsPrivateKey", "TlsCertificate"]
    for field in required_fields:
        if field not in data or not data[field]:  # type: ignore[literal-required]
            errors.append({"Error": f"{field}_missing", "Message": f"Missing required field: {field}"})

    if "GroupId" in data:
        group = Group.query.get(data["GroupId"])
        if not group:
            errors.append({"Error": "group_id_not_found", "Message": "Specified group ID does not exist."})

    try:
        onion_private_key = base64.b64decode(data["OnionPrivateKey"])
        onion_public_key = base64.b64decode(data["OnionPublicKey"])
    except (KeyError, ValueError, TypeError):
        errors.append({"Error": "onion_key_invalid", "Message": "Onion keys must be valid Base64-encoded data."})

    if errors:
        return jsonify({"Errors": errors}), 400

    skip_chain_verification = data.get("SkipChainVerification", False)
    skip_name_verification = data.get("SkipNameVerification", False)

    try:
        private_key = serialization.load_pem_private_key(
            data["TlsPrivateKey"].encode("utf-8"),
            password=None,
            backend=default_backend()
        )
        certificates = list(load_certificates_from_pem(data["TlsCertificate"].encode("utf-8")))
        if not certificates:
            errors.append(
                {"Error": "tls_public_key_invalid", "Message": "TLS public key must contain at least one certificate."})
            return jsonify({"Errors": errors}), 400

        chain = build_certificate_chain(certificates)
        end_entity_cert = chain[0]

        test_message = b"test"
        if not isinstance(private_key, RSAPrivateKey):
            errors.append({"Error": "tls_private_key_invalid", "Message": "Private key must be an RSA private key."})
            return jsonify({"Errors": errors}), 400
        hash_algorithm = end_entity_cert.signature_hash_algorithm
        if hash_algorithm is None:
            errors.append({"Error": "tls_public_key_invalid", "Message": "Public key using unsupported algorithm."})
            return jsonify({"Errors": errors}), 400
        signature = private_key.sign(
            test_message,
            PKCS1v15(),
            hash_algorithm
        )
        end_entity_public_key = end_entity_cert.public_key()
        if isinstance(end_entity_public_key, RSAPublicKey):
            end_entity_public_key.verify(
                signature,
                test_message,
                PKCS1v15(),
                hash_algorithm
            )
        else:
            errors.append({"Error": "tls_public_key_invalid", "Message": "Public key using unsupported algorithm."})

        if end_entity_cert.not_valid_after_utc < datetime.now(timezone.utc):
            errors.append({"Error": "tls_public_key_expired", "Message": "TLS public key is expired."})

        if end_entity_cert.not_valid_before_utc > datetime.now(timezone.utc):
            errors.append({"Error": "tls_public_key_future", "Message": "TLS public key is not yet valid."})

        try:
            san_extension = end_entity_cert.extensions.get_extension_for_oid(ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
            san_list = san_extension.value.get_values_for_type(x509.DNSName)  # type: ignore[attr-defined]
        except x509.ExtensionNotFound:
            san_list = []
            errors.append({"Error": "san_not_found",
                           "Message": "No Subject Alternative Names (SANs) found in the TLS public key."})

        if not skip_chain_verification:
            try:
                validate_certificate_chain(chain)
            except ValueError as e:
                errors.append({"Error": "certificate_chain_invalid", "Message": str(e)})

        if not skip_name_verification:
            if "DomainName" in data:
                registered_domain = tldextract.extract(data["DomainName"]).registered_domain
                if data["DomainName"] != registered_domain:
                    errors.append({"Error": "domain_name_not_registered_domain", "Message": "The domain name is invalid, or is not the top-level domain (i.e. a subdomain was entered)."})

            if "OnionPublicKey" in data:
                hostname = f"{onion_hostname(onion_public_key)}.onion"
                for name in [hostname, f"*.{hostname}"]:
                    if name not in san_list:
                        errors.append({"Error": "certificate_san_missing", "Message": f"{name} missing from certificate SAN list."})

        if errors:
            return jsonify({"Errors": errors}), 400

    except Exception as e:
        errors.append({"Error": "tls_validation_error", "Message": f"TLS key/certificate validation failed: {str(e)}"})
        return jsonify({"Errors": errors}), 400

    onion = Onion(
        domain_name=data["DomainName"].strip(),
        description=data["Description"],
        onion_private_key=onion_private_key,
        onion_public_key=onion_public_key,
        tls_private_key=data["TlsPrivateKey"].encode("utf-8"),
        tls_public_key=data["TlsCertificate"].encode("utf-8"),
        group_id=data["GroupId"],
        added=datetime.now(timezone.utc),
        updated=datetime.now(timezone.utc),
        # cert_expiry_date=end_entity_cert.not_valid_after_utc, TODO: extend schema to accommodate these fields
        # cert_sans=",".join(san_list),
    )

    try:
        db.session.add(onion)
        db.session.commit()
        return jsonify({"Message": "Onion service created successfully.", "Id": onion.id}), 201
    except exc.SQLAlchemyError as e:
        logging.exception(e)
        errors.append({"Error": "database_error", "Message": "Failed to create onion service."})
        return jsonify({"Errors": errors}), 500
