#!/usr/bin/env python3
"""
Tizen WGT signer - manual W3C Widget Digital Signature implementation.

Bypasses the broken password obfuscation in Tizen Studio CLI by signing
the WGT directly with the Samsung author + distributor .p12 certificates.

Produces:
  - author-signature.xml  (signed by author cert)
  - signature1.xml        (signed by distributor cert, references author-signature.xml too)
  - <name>.wgt            (zip of dist/ + both signatures)

Usage:
  sign_tizen_wgt.py --dist DIR --author-p12 FILE --author-pass PASS \
                    --dist-p12 FILE --dist-pass PASS --output FILE
"""
import argparse
import base64
import hashlib
import io
import os
import sys
import zipfile
from datetime import datetime, timezone
from pathlib import Path

from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import pkcs12
from lxml import etree


DSIG_NS = "http://www.w3.org/2000/09/xmldsig#"
DSP_NS = "http://www.w3.org/2009/xmldsig-properties"
WIDGETS_NS = "http://www.w3.org/ns/widgets-digsig"

C14N_ALG = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
SHA256_ALG = "http://www.w3.org/2001/04/xmlenc#sha256"
RSA_SHA256_ALG = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"

ROLE_AUTHOR = "http://www.w3.org/ns/widgets-digsig#role-author"
ROLE_DISTRIBUTOR = "http://www.w3.org/ns/widgets-digsig#role-distributor"
PROFILE_URI = "http://www.w3.org/ns/widgets-digsig#profile"


def load_p12(path: str, password: str):
    with open(path, "rb") as f:
        data = f.read()
    pwd = password.encode() if password else None
    key, cert, addl = pkcs12.load_key_and_certificates(data, pwd)
    if key is None or cert is None:
        raise RuntimeError(f"{path}: missing private key or certificate")
    return key, cert, addl or []


def file_sha256_b64(path: Path) -> str:
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            h.update(chunk)
    return base64.b64encode(h.digest()).decode()


def cert_to_b64(cert) -> str:
    der = cert.public_bytes(serialization.Encoding.DER)
    return base64.b64encode(der).decode()


def list_widget_files(dist_dir: Path, exclude=()):
    """Return sorted list of (relative_path_str, abs_path) for every regular
    file under dist_dir, excluding any in `exclude`. Order matches what
    'tizen package' produces."""
    excludes = {Path(e).as_posix() for e in exclude}
    files = []
    for p in sorted(dist_dir.rglob("*")):
        if not p.is_file():
            continue
        rel = p.relative_to(dist_dir).as_posix()
        if rel in excludes:
            continue
        files.append((rel, p))
    return files


def build_signature(role: str, sig_id: str, files, dist_dir: Path, key, cert,
                    extra_certs):
    """Build a W3C Widget Digital Signature XML document and sign it."""
    nsmap = {None: DSIG_NS}
    sig = etree.Element(f"{{{DSIG_NS}}}Signature", nsmap=nsmap)
    sig.set("Id", sig_id)

    signed_info = etree.SubElement(sig, f"{{{DSIG_NS}}}SignedInfo")
    etree.SubElement(signed_info, f"{{{DSIG_NS}}}CanonicalizationMethod",
                     Algorithm=C14N_ALG)
    etree.SubElement(signed_info, f"{{{DSIG_NS}}}SignatureMethod",
                     Algorithm=RSA_SHA256_ALG)

    # File references
    for rel, abs_path in files:
        ref = etree.SubElement(signed_info, f"{{{DSIG_NS}}}Reference",
                               URI=rel)
        etree.SubElement(ref, f"{{{DSIG_NS}}}DigestMethod",
                         Algorithm=SHA256_ALG)
        digest = etree.SubElement(ref, f"{{{DSIG_NS}}}DigestValue")
        digest.text = file_sha256_b64(abs_path)

    # Reference to the SignatureProperties Object (#prop)
    prop_ref = etree.SubElement(signed_info, f"{{{DSIG_NS}}}Reference",
                                URI="#prop")
    transforms = etree.SubElement(prop_ref, f"{{{DSIG_NS}}}Transforms")
    etree.SubElement(transforms, f"{{{DSIG_NS}}}Transform",
                     Algorithm=C14N_ALG)
    etree.SubElement(prop_ref, f"{{{DSIG_NS}}}DigestMethod",
                     Algorithm=SHA256_ALG)
    prop_digest = etree.SubElement(prop_ref, f"{{{DSIG_NS}}}DigestValue")
    # placeholder, computed after Object is built

    # SignatureValue placeholder
    sig_value = etree.SubElement(sig, f"{{{DSIG_NS}}}SignatureValue")

    # KeyInfo with cert chain
    key_info = etree.SubElement(sig, f"{{{DSIG_NS}}}KeyInfo")
    x509 = etree.SubElement(key_info, f"{{{DSIG_NS}}}X509Data")
    etree.SubElement(x509, f"{{{DSIG_NS}}}X509Certificate").text = cert_to_b64(cert)
    for c in extra_certs:
        etree.SubElement(x509, f"{{{DSIG_NS}}}X509Certificate").text = cert_to_b64(c)

    # Signature Properties Object
    obj = etree.SubElement(sig, f"{{{DSIG_NS}}}Object", Id="prop")
    sig_props = etree.SubElement(obj, f"{{{DSIG_NS}}}SignatureProperties")

    # Profile property
    sp_profile = etree.SubElement(sig_props, f"{{{DSIG_NS}}}SignatureProperty",
                                  Id="profile", Target=f"#{sig_id}")
    profile_el = etree.SubElement(
        sp_profile,
        f"{{{DSP_NS}}}Profile",
        nsmap={"dsp": DSP_NS},
        URI=PROFILE_URI,
    )

    # Role property
    sp_role = etree.SubElement(sig_props, f"{{{DSIG_NS}}}SignatureProperty",
                               Id="role", Target=f"#{sig_id}")
    role_el = etree.SubElement(
        sp_role,
        f"{{{DSP_NS}}}Role",
        nsmap={"dsp": DSP_NS},
        URI=role,
    )

    # Identifier (a timestamp string)
    sp_id = etree.SubElement(sig_props, f"{{{DSIG_NS}}}SignatureProperty",
                             Id="identifier", Target=f"#{sig_id}")
    id_el = etree.SubElement(
        sp_id,
        f"{{{DSP_NS}}}Identifier",
        nsmap={"dsp": DSP_NS},
    )
    id_el.text = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")

    # Compute the digest of the canonicalized Object element
    obj_c14n = etree.tostring(obj, method="c14n", exclusive=False)
    obj_digest = hashlib.sha256(obj_c14n).digest()
    prop_digest.text = base64.b64encode(obj_digest).decode()

    # Now canonicalize SignedInfo and sign
    si_c14n = etree.tostring(signed_info, method="c14n", exclusive=False)
    signature = key.sign(si_c14n, padding.PKCS1v15(), hashes.SHA256())
    sig_value.text = base64.b64encode(signature).decode()

    return sig


def signature_to_xml_bytes(sig_element) -> bytes:
    return (
        b'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
        + etree.tostring(sig_element, pretty_print=False)
    )


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--dist", required=True, help="Directory of files to package")
    ap.add_argument("--author-p12", required=True)
    ap.add_argument("--author-pass", required=True)
    ap.add_argument("--dist-p12", required=True)
    ap.add_argument("--dist-pass", required=True)
    ap.add_argument("--output", required=True, help="Output .wgt path")
    args = ap.parse_args()

    dist_dir = Path(args.dist).resolve()
    if not dist_dir.is_dir():
        sys.exit(f"--dist not a directory: {dist_dir}")

    print(f"Loading author cert from {args.author_p12}")
    author_key, author_cert, author_chain = load_p12(args.author_p12, args.author_pass)

    print(f"Loading distributor cert from {args.dist_p12}")
    dist_key, dist_cert, dist_chain = load_p12(args.dist_p12, args.dist_pass)

    # ---- author-signature.xml ----
    print("Building author-signature.xml ...")
    author_files = list_widget_files(dist_dir,
                                     exclude=("author-signature.xml", "signature1.xml"))
    print(f"  Files referenced by author signature: {len(author_files)}")
    author_sig = build_signature(
        role=ROLE_AUTHOR,
        sig_id="AuthorSignature",
        files=author_files,
        dist_dir=dist_dir,
        key=author_key,
        cert=author_cert,
        extra_certs=author_chain,
    )
    author_sig_bytes = signature_to_xml_bytes(author_sig)
    author_sig_path = dist_dir / "author-signature.xml"
    author_sig_path.write_bytes(author_sig_bytes)
    print(f"  Wrote {author_sig_path} ({len(author_sig_bytes)} bytes)")

    # ---- signature1.xml (distributor) ----
    # Distributor signs every file INCLUDING author-signature.xml
    print("Building signature1.xml ...")
    dist_files = list_widget_files(dist_dir, exclude=("signature1.xml",))
    print(f"  Files referenced by distributor signature: {len(dist_files)}")
    dist_sig = build_signature(
        role=ROLE_DISTRIBUTOR,
        sig_id="DistributorSignature",
        files=dist_files,
        dist_dir=dist_dir,
        key=dist_key,
        cert=dist_cert,
        extra_certs=dist_chain,
    )
    dist_sig_bytes = signature_to_xml_bytes(dist_sig)
    dist_sig_path = dist_dir / "signature1.xml"
    dist_sig_path.write_bytes(dist_sig_bytes)
    print(f"  Wrote {dist_sig_path} ({len(dist_sig_bytes)} bytes)")

    # ---- Build the .wgt zip ----
    print(f"Packaging .wgt -> {args.output}")
    output = Path(args.output).resolve()
    output.parent.mkdir(parents=True, exist_ok=True)
    with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as z:
        # config.xml MUST be first per W3C Widget spec
        config_xml = dist_dir / "config.xml"
        if not config_xml.is_file():
            sys.exit("config.xml missing in dist/")
        z.write(config_xml, "config.xml")
        # then the rest, then the signatures
        for rel, abs_path in sorted(list_widget_files(dist_dir, exclude=("config.xml",))):
            z.write(abs_path, rel)
    print(f"Done. Output: {output} ({output.stat().st_size} bytes)")


if __name__ == "__main__":
    main()
