#!/opt/pdftool-venv/bin/python
"""
PDF 數位簽章 / 增量蓋章 — 使用 pyHanko，以增量更新方式處理，保留既有簽章。

用法：
  pdftool_sign.py --ops ops.json --in in.pdf --out out.pdf --cert cert.pem --key key.pem
      ops.json = [ {"op":"stamp","page":1,"box":[x1,y1,x2,y2],"image":"a.png"},
                   {"op":"sign","field":"Sig1","reason":"...","location":"...","contact":"...",
                    "page":1,"box":[x1,y1,x2,y2],"image":"sig.png","text":""} ]
      （box 為 PDF 使用者座標，左下原點；省略 page/box 的 sign 為不可見簽章）
  pdftool_sign.py --verify in.pdf   -> JSON 列出簽章與驗證結果
"""
import argparse
import json
import os
import shutil
import sys
import tempfile


def _image_style(image, text=''):
    from pyhanko import stamp
    from pyhanko.pdf_utils import images
    from pyhanko.pdf_utils.layout import SimpleBoxLayoutRule, AxisAlignment, Margins

    return stamp.TextStampStyle(
        stamp_text=text or '',
        background=images.PdfImage(image),
        background_opacity=1.0,
        border_width=0,
        background_layout=SimpleBoxLayoutRule(
            x_align=AxisAlignment.ALIGN_MID,
            y_align=AxisAlignment.ALIGN_MID,
            margins=Margins.uniform(0),
        ),
    )


def apply_stamps(src, dst, stamps):
    from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
    from pyhanko.pdf_utils.layout import BoxConstraints
    from pyhanko.stamp import TextStamp

    with open(src, 'rb') as inf:
        w = IncrementalPdfFileWriter(inf)
        for s in stamps:
            x1, y1, x2, y2 = [float(v) for v in s['box']]
            style = _image_style(s['image'], s.get('text', ''))
            st = TextStamp(writer=w, style=style, box=BoxConstraints(width=x2 - x1, height=y2 - y1))
            st.apply(dest_page=int(s.get('page', 1)) - 1, x=x1, y=y1)
        with open(dst, 'wb') as outf:
            w.write(outf)


def apply_sign(src, dst, op, signer):
    from pyhanko.sign import signers, fields
    from pyhanko.sign.fields import SigFieldSpec
    from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
    from pyhanko import stamp

    with open(src, 'rb') as inf:
        w = IncrementalPdfFileWriter(inf)
        meta = signers.PdfSignatureMetadata(
            field_name=op.get('field') or 'Signature1',
            reason=op.get('reason') or None,
            location=op.get('location') or None,
            contact_info=op.get('contact') or None,
            md_algorithm='sha256',
        )
        stamp_style = None
        if op.get('page') and op.get('box'):
            x1, y1, x2, y2 = [float(v) for v in op['box']]
            fields.append_signature_field(
                w, SigFieldSpec(sig_field_name=meta.field_name, on_page=int(op['page']) - 1, box=(x1, y1, x2, y2))
            )
            if op.get('image'):
                stamp_style = _image_style(op['image'], op.get('text', ''))
            else:
                stamp_style = stamp.TextStampStyle(stamp_text=op.get('text') or '%(signer)s\n%(ts)s', border_width=1)
        pdf_signer = signers.PdfSigner(meta, signer=signer, stamp_style=stamp_style)
        with open(dst, 'wb') as outf:
            pdf_signer.sign_pdf(w, output=outf)


def do_ops(args):
    from pyhanko.sign import signers

    with open(args.ops, 'r', encoding='utf-8') as f:
        ops = json.load(f)
    if not isinstance(ops, list) or not ops:
        raise SystemExit('ops 為空')

    signer = None
    if any(o.get('op') == 'sign' for o in ops):
        signer = signers.SimpleSigner.load(args.key, args.cert, key_passphrase=None)
        if signer is None:
            raise SystemExit('無法載入簽章憑證或私鑰')

    tmpdir = tempfile.mkdtemp(prefix='pdfsign_')
    try:
        cur = os.path.join(tmpdir, 'step0.pdf')
        shutil.copyfile(args.inp, cur)
        step = 0
        pending_stamps = []
        done = []

        def flush_stamps():
            nonlocal cur, step
            if not pending_stamps:
                return
            step += 1
            nxt = os.path.join(tmpdir, f'step{step}.pdf')
            apply_stamps(cur, nxt, pending_stamps)
            cur = nxt
            pending_stamps.clear()

        for o in ops:
            if o.get('op') == 'stamp':
                pending_stamps.append(o)
                continue
            if o.get('op') == 'sign':
                flush_stamps()
                step += 1
                nxt = os.path.join(tmpdir, f'step{step}.pdf')
                apply_sign(cur, nxt, o, signer)
                cur = nxt
                done.append(o.get('field'))
        flush_stamps()
        shutil.copyfile(cur, args.out)
    finally:
        shutil.rmtree(tmpdir, ignore_errors=True)
    print(json.dumps({'ok': True, 'signed_fields': done}))


def do_verify(path):
    from pyhanko.pdf_utils.reader import PdfFileReader
    from pyhanko.sign.validation import validate_pdf_signature
    from pyhanko_certvalidator import ValidationContext

    result = []
    with open(path, 'rb') as f:
        r = PdfFileReader(f)
        for sig in r.embedded_signatures:
            item = {'field': sig.field_name, 'signer': None, 'signed_at': None, 'reason': None,
                    'intact': None, 'valid': None, 'trusted': None, 'coverage': None, 'summary': None}
            try:
                cert = sig.signer_cert
                item['signer'] = cert.subject.human_friendly if cert is not None else None
            except Exception:
                pass
            try:
                obj = sig.sig_object
                item['reason'] = str(obj.get('/Reason')) if obj.get('/Reason') else None
                item['signed_at'] = str(obj.get('/M')) if obj.get('/M') else None
            except Exception:
                pass
            try:
                vc = ValidationContext(allow_fetching=False)
                status = validate_pdf_signature(sig, vc)
                item['intact'] = bool(status.intact)
                item['valid'] = bool(status.valid)
                item['trusted'] = bool(status.trusted)
                item['coverage'] = str(status.coverage).split('.')[-1] if status.coverage is not None else None
                item['summary'] = status.summary()
            except Exception as e:
                item['summary'] = 'UNVERIFIED: ' + str(e)[:200]
            result.append(item)
    print(json.dumps({'ok': True, 'signatures': result}, ensure_ascii=False))


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--verify', help='只驗證此 PDF 的簽章')
    ap.add_argument('--ops', help='操作清單 JSON 檔')
    ap.add_argument('--in', dest='inp')
    ap.add_argument('--out')
    ap.add_argument('--cert')
    ap.add_argument('--key')
    args = ap.parse_args()
    try:
        if args.verify:
            do_verify(args.verify)
        else:
            if not (args.ops and args.inp and args.out):
                raise SystemExit('缺少必要參數')
            do_ops(args)
    except SystemExit:
        raise
    except Exception as e:
        print(json.dumps({'ok': False, 'error': str(e)}, ensure_ascii=False))
        sys.exit(2)


if __name__ == '__main__':
    main()
