"""CLI del xamiSDK.

Comandos (Fase 1):
  xamisdk init      -> ceremonia de pareo con Xami (genera llaves, intercambia
                       publicas, guarda token y config).
  xamisdk status    -> estado del pareo.

Los textos al usuario van en espanol (interfaz); el codigo/estado en ingles.
Ver docs/servicios_xamiSDK.md.
"""
from __future__ import annotations
import argparse
import getpass
import os
import sys

from xamisdk import __version__
from xamisdk.config import Config
from xamisdk.cache import Cache
from xamisdk.signer import Signer, SignerError
from xamisdk.results import Results
from xamisdk.wrappers import Wrappers
from xamisdk import crypto
from xamisdk.client import XamiClient, XamiError, host_fingerprint


def _cmd_init(args):
    cfg = Config()
    cfg.ensure_dirs()

    if cfg.is_paired() and not args.force:
        print("Este xamiSDK ya esta pareado. Usa --force para rehacer la ceremonia.")
        return 1

    print("== Ceremonia de pareo del xamiSDK ==\n")

    # 1. matricula
    sdk_key = args.sdk_key or input("SDK Key (xsdk_live_...): ").strip()
    secret = args.secret or getpass.getpass("Secret (no se mostrara): ").strip()
    if not sdk_key or not secret:
        print("Error: se requieren SDK Key y Secret.")
        return 1

    receive_url = args.receive_url
    if receive_url is None:
        receive_url = input("URL de recepcion del push (enter para omitir): ").strip() or None

    endpoint = args.endpoint or "https://api.xami.run"

    # 2. generar par RSA local (la privada nunca sale)
    print("\nGenerando par de llaves local (RSA-3072)...")
    priv_pem, pub_pem = crypto.generate_keypair()
    cfg.save_sdk_keypair(priv_pem, pub_pem)
    print("  Llaves generadas. La privada queda local (chmod 600).")

    # 3. parear con Xami
    print(f"\nContactando a Xami ({endpoint})...")
    client = XamiClient(endpoint)
    fp = host_fingerprint()
    try:
        resp = client.pair(sdk_key, secret, pub_pem, receive_url, __version__, fp)
    except XamiError as e:
        print(f"  Error en el pareo: {e}")
        return 1

    if not resp.get("paired"):
        print(f"  El pareo no fue exitoso: {resp}")
        return 1

    # 4. guardar la publica de Xami + token + estado
    cfg.save_xami_public(resp["xami_pubkey"])
    cfg.update_state(
        paired=True,
        sdk_key=sdk_key,
        tenant_id=resp.get("tenant_id"),
        endpoint=endpoint,
        receive_url=receive_url,
        access_token=resp.get("access_token"),
        token_expires_in=resp.get("expires_in"),
        host_fingerprint=fp,
        sdk_version=__version__,
    )

    print("\n  Pareo completado.")
    print(f"    tenant_id : {resp.get('tenant_id')}")
    print(f"    token     : valido por {resp.get('expires_in')} s")
    print(f"    publica de Xami: guardada")
    print("\nEl xamiSDK esta listo. Verifica con: xamisdk status")
    return 0


def _cmd_status(args):
    cfg = Config()
    st = cfg.load_state()
    if not st.get("paired"):
        print("Estado: NO pareado. Ejecuta 'xamisdk init'.")
        return 0
    print("Estado: PAREADO")
    print(f"  SDK Key      : {st.get('sdk_key')}")
    print(f"  Tenant       : {st.get('tenant_id')}")
    print(f"  Endpoint     : {st.get('endpoint')}")
    print(f"  Receive URL  : {st.get('receive_url') or '(no registrada)'}")
    print(f"  Host FP      : {st.get('host_fingerprint')}")
    print(f"  Version      : {st.get('sdk_version')}")
    has_xami = cfg.has_keys()
    print(f"  Llaves       : {'OK' if has_xami else 'faltan'}")
    return 0


def _cmd_cache_show(args):
    cfg = Config()
    c = Cache(cfg.base_dir)
    su = c.summary()
    print("Cache local del xamiSDK:")
    print(f"  Credenciales : {su['credentials']}")
    print(f"  Disenos      : {su['designs']}")
    return 0


def _cmd_credentials_list(args):
    cfg = Config()
    creds = Cache(cfg.base_dir).list_credentials()
    if not creds:
        print("No hay credenciales en cache. Xami las empuja al cambiarlas en la consola.")
        return 0
    print(f"{len(creds)} credencial(es) en cache:")
    for cr in creds:
        key = cr.get("credential_key") or cr.get("key") or "?"
        print(f"  {key}  tipo={cr.get('type','?')}  device={cr.get('device_id','-')}  default_design={cr.get('default_design_key','-')}")
    return 0


def _cmd_designs_list(args):
    cfg = Config()
    designs = Cache(cfg.base_dir).list_designs()
    if not designs:
        print("No hay disenos en cache. Xami los empuja al cambiarlos en la consola.")
        return 0
    print(f"{len(designs)} diseno(s) en cache:")
    for d in designs:
        key = d.get("design_key") or d.get("key") or "?"
        print(f"  {key}  nombre={d.get('name','-')}  version={d.get('version','-')}")
    return 0


def _cmd_designs_show(args):
    cfg = Config()
    d = Cache(cfg.base_dir).get_design(args.design_key)
    if not d:
        print(f"No hay un diseno con key {args.design_key} en cache.")
        return 1
    import json as _json
    # ocultar la imagen (puede ser enorme) mostrando solo su tamano
    dd = _json.loads(_json.dumps(d))
    params = dd.get("params") or {}
    img = params.get("image")
    if isinstance(img, str) and len(img) > 40:
        params["image"] = f"<{len(img)} bytes>"
    print(_json.dumps(dd, indent=2, ensure_ascii=False))
    return 0


def _cmd_pades_sign(args):
    cfg = Config()
    if not cfg.is_paired():
        print("El xamiSDK no esta pareado. Ejecuta 'xamisdk init' primero.")
        return 1
    if not os.path.isfile(args.pdf):
        print(f"No existe el archivo: {args.pdf}")
        return 1
    with open(args.pdf, "rb") as f:
        pdf_bytes = f.read()

    signer = Signer(cfg)
    print(f"Preparando y enviando el hash a Xami (el documento NO sale)...")
    try:
        rid = signer.sign_pdf(pdf_bytes, args.credential_key,
                              design_key=args.design,
                              variables=_parse_vars(args.var),
                              reason=args.reason, location=args.location,
                              signer_name=args.signer_name)
    except SignerError as e:
        print(f"Error al enviar: {e}")
        return 1
    print(f"  requestId: {rid}")

    if not args.wait:
        print("Encolado. Consulta el resultado con: xamisdk callbacks:show " + rid)
        return 0

    print("Esperando la firma del chip (via push)...")
    res = signer.wait(rid, timeout=args.timeout)
    status = res.get("status")
    if status == "DONE":
        src = res.get("output_path")
        out = args.out or _default_out(args.pdf)
        import shutil
        shutil.copyfile(src, out)
        print(f"  Firmado -> {out}")
        return 0
    elif status == "ERROR":
        print(f"  Error: {res.get('error')}")
        return 1
    else:
        print(f"  Sin respuesta a tiempo (estado {status}). Reintenta con callbacks:show {rid}")
        return 1


def _cmd_callbacks_list(args):
    cfg = Config()
    items = Results(cfg.base_dir).list()
    if not items:
        print("No hay callbacks registrados.")
        return 0
    print(f"{len(items)} callback(s):")
    for r in items:
        print(f"  {r.get('request_id')}  [{r.get('status')}]  {r.get('output_path','')}")
    return 0


def _cmd_callbacks_show(args):
    cfg = Config()
    r = Results(cfg.base_dir).get(args.request_id)
    if not r:
        print(f"No hay un callback con id {args.request_id}.")
        return 1
    import json as _json
    print(_json.dumps(r, indent=2, ensure_ascii=False))
    return 0


def _parse_vars(var_list):
    """Convierte ['name=Jose','cargo=CTO'] en {'name':'Jose','cargo':'CTO'}."""
    out = {}
    for item in (var_list or []):
        if "=" in item:
            k, _, v = item.partition("=")
            out[k.strip()] = v
    return out


def _default_out(pdf_path):
    base, ext = os.path.splitext(pdf_path)
    return base + "_firmado" + (ext or ".pdf")


def _cmd_wrapper_create(args):
    cfg = Config()
    w = Wrappers(cfg.base_dir).create(args.name)
    print("Credencial de wrapper creada (guarda el secret, no se vuelve a mostrar):")
    print(f"  wrapper_key : {w['wrapper_key']}")
    print(f"  secret      : {w['secret']}")
    print(f"  nombre      : {w['name']}")
    return 0


def _cmd_wrapper_list(args):
    cfg = Config()
    ws = Wrappers(cfg.base_dir).list()
    if not ws:
        print("No hay credenciales de wrapper. Crea una con 'xamisdk wrapper:create --name ...'.")
        return 0
    print(f"{len(ws)} credencial(es) de wrapper:")
    for w in ws:
        import datetime as _dt
        lu = w.get("last_used_at")
        lu_s = _dt.datetime.fromtimestamp(lu).strftime("%Y-%m-%d %H:%M") if lu else "nunca"
        print(f"  {w['wrapper_key']}  {w['name']}  [{w['status']}]  ult. uso: {lu_s}")
    return 0


def _cmd_wrapper_revoke(args):
    cfg = Config()
    ok = Wrappers(cfg.base_dir).revoke(args.wrapper_key)
    if ok:
        print(f"Credencial {args.wrapper_key} revocada. Esa app ya no puede usar el xamiSDK.")
        return 0
    print(f"No se encontro una credencial activa con key {args.wrapper_key}.")
    return 1


def _cmd_serve(args):
    import uvicorn
    from xamisdk.service import create_app
    cfg = Config()
    app = create_app(cfg)
    print(f"xamiSDK escuchando en {args.host}:{args.port}")
    print(f"  /receive        <- push de Xami")
    print(f"  /v1/pades/sign  <- wrappers locales")

    # Aviso si el receive_url guardado en el pareo no coincide con este host:puerto.
    # Sin esto, un push de Xami que no puede alcanzar el receive_url falla en
    # silencio (solo un log.warning del lado del servidor) y toda firma se queda
    # esperando el resultado para siempre, sin ningun error visible para el usuario.
    state = cfg.load_state()
    saved_url = (state.get("receive_url") or "").strip()
    host_for_check = "127.0.0.1" if args.host in ("0.0.0.0", "127.0.0.1") else args.host
    expected = f"http://{host_for_check}:{args.port}/receive"
    if not saved_url:
        print("\n  ⚠  ADVERTENCIA: no hay receive_url configurado en el pareo.")
        print("     Xami no podra empujarte los resultados de firma; toda firma se")
        print("     quedara esperando indefinidamente (timeout silencioso).")
        print(f"     Solucion: xamisdk init --force --receive-url {expected}\n")
    elif saved_url.rstrip("/") != expected.rstrip("/"):
        print(f"\n  ⚠  ADVERTENCIA: el receive_url pareado ({saved_url}) no coincide")
        print(f"     con donde estas sirviendo ahora ({expected}).")
        print("     Los resultados de firma no te llegaran mientras no coincidan.")
        print(f"     Solucion: xamisdk init --force --receive-url {expected}\n")

    uvicorn.run(app, host=args.host, port=args.port, log_level="warning")
    return 0


def _cmd_blockchain_sign(args):
    cfg = Config()
    if not cfg.is_paired():
        print("El xamiSDK no esta pareado. Ejecuta 'xamisdk init' primero.")
        return 1
    signer = Signer(cfg)
    print("Enviando el hash de la tx a Xami (el chip firma con la clave EVM)...")
    try:
        rid = signer.sign_blockchain(args.hash, args.credential_key, chain_id=args.chain_id)
    except SignerError as e:
        print(f"Error: {e}")
        return 1
    print(f"  requestId: {rid}")
    if not args.wait:
        print("Encolado. Consulta el resultado con: xamisdk callbacks:show " + rid)
        return 0
    print("Esperando la firma del chip (via push)...")
    res = signer.wait(rid, timeout=args.timeout)
    if res.get("status") == "DONE":
        print(f"  r: {res.get('r')}")
        print(f"  s: {res.get('s')}")
        print(f"  v: {res.get('v')}")
        print(f"  signature: {res.get('signature')}")
        return 0
    elif res.get("status") == "ERROR":
        print(f"  Error: {res.get('error')}")
        return 1
    print(f"  Sin respuesta a tiempo. Reintenta con callbacks:show {rid}")
    return 1


def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(prog="xamisdk", description="xamiSDK - firma local sin que tus documentos salgan de tu red")
    p.add_argument("--version", action="version", version=f"xamisdk {__version__}")
    sub = p.add_subparsers(dest="cmd", required=True)

    pi = sub.add_parser("init", help="ceremonia de pareo con Xami")
    pi.add_argument("--sdk-key", dest="sdk_key", help="matricula (xsdk_live_...)")
    pi.add_argument("--secret", help="secret de la matricula")
    pi.add_argument("--receive-url", dest="receive_url", help="URL donde Xami empujara los mensajes")
    pi.add_argument("--endpoint", help="endpoint de Xami (por defecto https://api.xami.run)")
    pi.add_argument("--force", action="store_true", help="rehacer la ceremonia aunque ya este pareado")
    pi.set_defaults(func=_cmd_init)

    ps = sub.add_parser("status", help="estado del pareo")
    ps.set_defaults(func=_cmd_status)

    pcs = sub.add_parser("cache:show", help="resumen de la cache local")
    pcs.set_defaults(func=_cmd_cache_show)

    pcl = sub.add_parser("credentials:list", help="credenciales en cache")
    pcl.set_defaults(func=_cmd_credentials_list)

    pdl = sub.add_parser("designs:list", help="disenos en cache")
    pdl.set_defaults(func=_cmd_designs_list)

    pds = sub.add_parser("designs:show", help="detalle de un diseno en cache")
    pds.add_argument("design_key", help="clave del diseno")
    pds.set_defaults(func=_cmd_designs_show)

    psign = sub.add_parser("pades:sign", help="firmar un PDF (el documento no sale)")
    psign.add_argument("pdf", help="ruta del PDF a firmar")
    psign.add_argument("--credential-key", dest="credential_key", required=True, help="credencial a usar")
    psign.add_argument("--design", help="design_key (si no, se usa el default de la credencial)")
    psign.add_argument("--var", action="append", help="variable del sello, formato clave=valor (repetible)")
    psign.add_argument("--reason", help="razon de la firma")
    psign.add_argument("--location", help="lugar de la firma")
    psign.add_argument("--signer-name", dest="signer_name", help="nombre del firmante")
    psign.add_argument("--out", help="ruta de salida del PDF firmado")
    psign.add_argument("--wait", action="store_true", help="esperar el resultado y guardar el PDF")
    psign.add_argument("--timeout", type=float, default=60.0, help="segundos de espera en modo --wait")
    psign.set_defaults(func=_cmd_pades_sign)

    pcbl = sub.add_parser("callbacks:list", help="resultados de firma recibidos por push")
    pcbl.set_defaults(func=_cmd_callbacks_list)

    pcbs = sub.add_parser("callbacks:show", help="detalle de un resultado de firma")
    pcbs.add_argument("request_id", help="id de la operacion")
    pcbs.set_defaults(func=_cmd_callbacks_show)

    pwc = sub.add_parser("wrapper:create", help="emitir una credencial de wrapper")
    pwc.add_argument("--name", required=True, help="nombre de la app/wrapper")
    pwc.set_defaults(func=_cmd_wrapper_create)

    pwl = sub.add_parser("wrapper:list", help="credenciales de wrapper emitidas")
    pwl.set_defaults(func=_cmd_wrapper_list)

    pwr = sub.add_parser("wrapper:revoke", help="revocar una credencial de wrapper")
    pwr.add_argument("wrapper_key", help="key de la credencial (wrp_...)")
    pwr.set_defaults(func=_cmd_wrapper_revoke)

    pbc = sub.add_parser("blockchain:sign", help="firmar el hash de una tx (EVM)")
    pbc.add_argument("hash", help="keccak256 de la tx (64 hex, con o sin 0x)")
    pbc.add_argument("--credential-key", dest="credential_key", required=True, help="credencial EVM (wallet)")
    pbc.add_argument("--chain-id", dest="chain_id", type=int, help="chain id (EIP-155)")
    pbc.add_argument("--wait", action="store_true", help="esperar r/s/v")
    pbc.add_argument("--timeout", type=float, default=60.0)
    pbc.set_defaults(func=_cmd_blockchain_sign)

    psv = sub.add_parser("serve", help="levantar el servicio local del xamiSDK")
    psv.add_argument("--host", default="127.0.0.1")
    psv.add_argument("--port", type=int, default=8300)
    psv.set_defaults(func=_cmd_serve)

    return p


def main(argv=None):
    parser = build_parser()
    args = parser.parse_args(argv)
    return args.func(args)


if __name__ == "__main__":
    sys.exit(main())
