#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys
import json
from pathlib import Path
from typing import Tuple

import datetime
import base64

from lxml import etree
from cryptography import x509
from cryptography.hazmat.primitives.serialization import pkcs12, Encoding, PrivateFormat, NoEncryption
from cryptography.hazmat.primitives import hashes
import xmlsec
from xmlsec import constants as XCON


# ------------------ helpers ------------------

def load_key_and_cert_from_pfx(pfx_path: Path, pfx_password: bytes):
    privkey, cert, _ = pkcs12.load_key_and_certificates(pfx_path.read_bytes(), pfx_password)
    if privkey is None or cert is None:
        raise RuntimeError("Nie udało się załadować klucza/certyfikatu z PFX.")
    key_pem = privkey.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())
    cert_pem = cert.public_bytes(Encoding.PEM)
    return key_pem, cert_pem, cert


def cert_digest_sha256_b64(cert: x509.Certificate) -> str:
    digest = cert.fingerprint(hashes.SHA256())
    return base64.b64encode(digest).decode("ascii")


def build_xades_object(signature_id: str, signed_props_id: str, cert: x509.Certificate):
    """
    Buduje <ds:Object> zawierający strukturę XAdES-BES:
      <xades:QualifyingProperties Target="#Signature-1">
        <xades:SignedProperties Id="SignedProperties-1">
          <xades:SignedSignatureProperties>
            <xades:SigningTime>...</xades:SigningTime>
            <xades:SigningCertificate>...</xades:SigningCertificate>
          </xades:SignedSignatureProperties>
        </xades:SignedProperties>
      </xades:QualifyingProperties>
    Zwraca krotkę (object_element, signed_properties_element)
    """
    DS_NS = "http://www.w3.org/2000/09/xmldsig#"
    XADES_NS = "http://uri.etsi.org/01903/v1.3.2#"
    ds = "{%s}" % DS_NS
    xades = "{%s}" % XADES_NS

    obj = etree.Element(f"{ds}Object")

    qp = etree.SubElement(obj, f"{xades}QualifyingProperties")
    qp.set("Target", f"#{signature_id}")

    sp = etree.SubElement(qp, f"{xades}SignedProperties")
    sp.set("Id", signed_props_id)

    ssp = etree.SubElement(sp, f"{xades}SignedSignatureProperties")

    st = etree.SubElement(ssp, f"{xades}SigningTime")
    st.text = datetime.datetime.now(datetime.UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")

    sc = etree.SubElement(ssp, f"{xades}SigningCertificate")
    cert_el = etree.SubElement(sc, f"{xades}Cert")

    cd = etree.SubElement(cert_el, f"{xades}CertDigest")
    dm = etree.SubElement(cd, f"{ds}DigestMethod")
    dm.set("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256")
    dv = etree.SubElement(cd, f"{ds}DigestValue")
    dv.text = cert_digest_sha256_b64(cert)

    iser = etree.SubElement(cert_el, f"{xades}IssuerSerial")
    iname = etree.SubElement(iser, f"{ds}X509IssuerName")
    iname.text = cert.issuer.rfc4514_string()
    snum = etree.SubElement(iser, f"{ds}X509SerialNumber")
    snum.text = str(cert.serial_number)

    return obj, sp


def sign_auth_token_request_enveloped(
    unsigned_xml_bytes: bytes,
    key_pem: bytes,
    cert_pem: bytes,
    cert_obj: x509.Certificate,
) -> bytes:
    """
    Robi dokładnie to, co w Twoim działającym kodzie:
    - dodaje enveloped ds:Signature Id="Signature-1"
    - dodaje referencję URI="" (enveloped + excl c14n)
    - dodaje referencję do SignedProperties z typem XAdES
    - wstawia KeyInfo/X509Data
    - podpisuje całość RSA-SHA256 (albo ECDSA-SHA256 jeśli klucz to EC)

    Zwraca finalny AuthTokenRequest jako bajty UTF-8 gotowe do wysłania do /auth/xades-signature
    """

    parser = etree.XMLParser(remove_blank_text=False)
    root = etree.fromstring(unsigned_xml_bytes, parser=parser)
    tree = etree.ElementTree(root)

    # Tworzymy <ds:Signature ...>
    sig = xmlsec.template.create(
        tree,
        c14n_method=XCON.TransformExclC14N,
        sign_method=XCON.TransformRsaSha256
    )
    sig.set("Id", "Signature-1")

    # Enveloped signature -> podpis wewnątrz AuthTokenRequest
    root.append(sig)

    # Referencja do całego dokumentu (URI = "")
    ref_doc = xmlsec.template.add_reference(sig, XCON.TransformSha256, uri="")
    xmlsec.template.add_transform(ref_doc, XCON.TransformEnveloped)
    xmlsec.template.add_transform(ref_doc, XCON.TransformExclC14N)

    # KeyInfo/X509
    ki = xmlsec.template.ensure_key_info(sig)
    xmlsec.template.add_x509_data(ki)

    # Dodaj XAdES-BES i referencję do SignedProperties
    xades_object, signed_props_el = build_xades_object("Signature-1", "SignedProperties-1", cert_obj)
    sig.append(xades_object)

    ref_props = xmlsec.template.add_reference(
        sig,
        XCON.TransformSha256,
        uri="#SignedProperties-1"
    )
    ref_props.set("Type", "http://uri.etsi.org/01903#SignedProperties")
    xmlsec.template.add_transform(ref_props, XCON.TransformExclC14N)

    # Przygotuj kontekst podpisu
    ctx = xmlsec.SignatureContext()

    key = xmlsec.Key.from_memory(key_pem, xmlsec.KeyFormat.PEM, None)
    key.load_cert_from_memory(cert_pem, xmlsec.KeyFormat.PEM)
    ctx.key = key

    # Obsługa ECDSA vs RSA
    try:
        if cert_obj.public_key().__class__.__name__.lower().startswith("ec"):
            xmlsec.tree.set_signature_method(sig, XCON.TransformEcdsaSha256)
    except Exception:
        pass

    # Podpisz
    ctx.sign(sig)

    # Gotowe, zwróć pełen dokument (AuthTokenRequest z wstawionym ds:Signature)
    return etree.tostring(
        tree,
        encoding="UTF-8",
        xml_declaration=True,
        pretty_print=True
    )


def main():
    # args:
    #   1: unsigned_xml_path
    #   2: pfx_path
    #   3: pfx_password
    #   4: output_signed_path
    if len(sys.argv) != 5:
        print(json.dumps({"ok": False, "error": "usage: sign_auth_token_request.py unsigned.xml cert.pfx pfxPass output.xml"}))
        sys.exit(1)

    unsigned_xml_path = Path(sys.argv[1]).resolve()
    pfx_path          = Path(sys.argv[2]).resolve()
    pfx_password      = sys.argv[3].encode("utf-8")
    output_signed     = Path(sys.argv[4]).resolve()

    if not unsigned_xml_path.exists():
        print(json.dumps({"ok": False, "error": f"Brak pliku unsigned: {unsigned_xml_path}"}))
        sys.exit(2)

    if not pfx_path.exists():
        print(json.dumps({"ok": False, "error": f"Brak PFX: {pfx_path}"}))
        sys.exit(3)

    # 1) wczytaj unsigned AuthTokenRequest
    unsigned_xml_bytes = unsigned_xml_path.read_bytes()

    # 2) wyciągnij klucz i cert
    key_pem, cert_pem, cert_obj = load_key_and_cert_from_pfx(pfx_path, pfx_password)

    # 3) podpis XAdES-BES enveloped
    signed_xml_bytes = sign_auth_token_request_enveloped(
        unsigned_xml_bytes,
        key_pem,
        cert_pem,
        cert_obj
    )

    # 4) zapisz wynik do output_signed_path
    output_signed.parent.mkdir(parents=True, exist_ok=True)
    output_signed.write_bytes(signed_xml_bytes)

    # 5) wypisz JSON na stdout
    print(json.dumps({
        "ok": True,
        "signed_path": str(output_signed)
    }, ensure_ascii=False))

    sys.exit(0)


if __name__ == "__main__":
    main()
