# apps/aroflo_connector_app/zones/users/mutations.py
from __future__ import annotations

import sys, subprocess
from typing import Any, Dict, List, Tuple, Optional

from ..base import ZoneOperation, ParamSpec
from ._join_utils import request, raw_wrap


# -------------------------
# Operation codes
# -------------------------
OP_CREATE = "create_user"
OP_UPDATE = "update_user"
OP_UPDATE_CUSTOMFIELDS = "update_user_customfields"
OP_UPDATE_PERMISSIONGROUPS = "update_user_permissiongroups"
OP_UPDATE_FEATUREACCESS = "update_user_featureaccess"
OP_UI_UPLOAD_DOCS = "ui_upload_user_documents"


AF_ZONE_USERS = "users"


# -------------------------
# Helpers
# -------------------------
def _cdata(s: Optional[str]) -> str:
    if s is None:
        return ""
    return f"<![CDATA[{s}]]>"


def _require_non_empty_str(p: Dict[str, Any], key: str) -> str:
    v = p.get(key)
    if v is None or not str(v).strip():
        raise ValueError(f"{key} es requerido y no puede estar vacío.")
    return str(v).strip()


def _bool_text(v: Any) -> str:
    return "true" if bool(v) else "false"


def _build_postxml_create(p: Dict[str, Any]) -> str:
    # Required per doc
    givennames = _require_non_empty_str(p, "givennames")
    surname = _require_non_empty_str(p, "surname")
    username = _require_non_empty_str(p, "username")
    password = _require_non_empty_str(p, "password")

    org = p.get("org") or {}
    if not isinstance(org, dict):
        raise ValueError("org debe ser un objeto: {orgid: ...}")
    orgid = org.get("orgid")
    if not orgid or not str(orgid).strip():
        raise ValueError("org.orgid es requerido para create_user.")

    parts: List[str] = ["<users><user>"]

    parts.append(f"<givennames>{_cdata(givennames)}</givennames>")
    parts.append(f"<surname>{_cdata(surname)}</surname>")
    parts.append(f"<username>{_cdata(username)}</username>")
    parts.append(f"<password>{_cdata(password)}</password>")
    parts.append(f"<org><orgid>{orgid}</orgid></org>")

    # Optional fields (seguros)
    for k in ("email", "email2", "phone", "fax", "mobile", "position", "accesstype"):
        if p.get(k) is not None:
            parts.append(f"<{k}>{_cdata(str(p.get(k)))}</{k}>")

    # address (opcional)
    address = p.get("address")
    if isinstance(address, dict) and address:
        addr_parts: List[str] = ["<address>"]
        for k in ("addressline1", "addressline2", "suburb", "state", "postcode", "country"):
            if address.get(k) is not None:
                addr_parts.append(f"<{k}>{_cdata(str(address.get(k)))}</{k}>")
        addr_parts.append("</address>")
        parts.append("".join(addr_parts))

    # permissiongroups (opcional)
    permissiongroups = p.get("permissiongroups")
    if isinstance(permissiongroups, list) and permissiongroups:
        pg_parts: List[str] = ["<permissiongroups>"]
        for g in permissiongroups:
            if not isinstance(g, dict):
                raise ValueError("permissiongroups debe ser lista de objetos {groupid: ...}.")
            gid = g.get("groupid")
            if gid is None or not str(gid).strip():
                raise ValueError("Cada permissiongroup requiere groupid.")
            pg_parts.append(f"<permissiongroup><groupid>{gid}</groupid></permissiongroup>")
        pg_parts.append("</permissiongroups>")
        parts.append("".join(pg_parts))

    # customfields (opcional)
    customfields = p.get("customfields")
    if isinstance(customfields, list) and customfields:
        cf_parts: List[str] = ["<customfields>"]
        for cf in customfields:
            if not isinstance(cf, dict):
                raise ValueError("customfields debe ser lista de objetos.")
            cf_parts.append("<customfield>")
            if cf.get("fieldid") is not None:
                cf_parts.append(f"<fieldid>{cf['fieldid']}</fieldid>")
            if cf.get("name") is not None:
                cf_parts.append(f"<name>{_cdata(str(cf['name']))}</name>")
            if cf.get("type") is not None:
                cf_parts.append(f"<type>{_cdata(str(cf['type']))}</type>")
            if cf.get("value") is not None:
                cf_parts.append(f"<value>{_cdata(str(cf['value']))}</value>")
            cf_parts.append("</customfield>")
        cf_parts.append("</customfields>")
        parts.append("".join(cf_parts))

    # featureaccess (opcional)
    featureaccess = p.get("featureaccess")
    if isinstance(featureaccess, list) and featureaccess:
        fa_parts: List[str] = ["<featureaccess>"]
        for f in featureaccess:
            if not isinstance(f, dict):
                raise ValueError("featureaccess debe ser lista de objetos {featureid, featurevalue}.")
            fid = f.get("featureid")
            fval = f.get("featurevalue")
            if fid is None or fval is None:
                raise ValueError("Cada featureaccess requiere featureid y featurevalue.")
            fa_parts.append("<feature>")
            fa_parts.append(f"<featureid>{fid}</featureid>")
            fa_parts.append(f"<featurevalue>{_cdata(str(fval))}</featurevalue>")
            fa_parts.append("</feature>")
        fa_parts.append("</featureaccess>")
        parts.append("".join(fa_parts))

    parts.append("</user></users>")
    return "".join(parts)


def _build_postxml_update(p: Dict[str, Any]) -> str:
    userid = _require_non_empty_str(p, "userid")

    # Se permite update parcial: al menos un campo adicional
    allowed_keys = {
        "givennames", "surname", "username", "email", "email2", "phone", "fax", "mobile",
        "position", "accesstype", "archived", "address", "org",
        "permissiongroups", "customfields", "featureaccess",
    }
    has_any = any(k in p and p.get(k) is not None for k in allowed_keys)
    if not has_any:
        raise ValueError("update_user requiere al menos un campo a actualizar además de userid.")

    parts: List[str] = ["<users><user>"]
    parts.append(f"<userid>{userid}</userid>")

    # scalars
    for k in ("givennames", "surname", "username", "email", "email2", "phone", "fax", "mobile", "position", "accesstype"):
        if p.get(k) is not None:
            parts.append(f"<{k}>{_cdata(str(p.get(k)))}</{k}>")

    # archived boolean
    if "archived" in p and p.get("archived") is not None:
        parts.append(f"<archived>{_bool_text(p.get('archived'))}</archived>")

    # org (opcional)
    org = p.get("org")
    if isinstance(org, dict) and org.get("orgid"):
        parts.append(f"<org><orgid>{org['orgid']}</orgid></org>")

    # address (opcional)
    address = p.get("address")
    if isinstance(address, dict) and address:
        addr_parts: List[str] = ["<address>"]
        for k in ("addressline1", "addressline2", "suburb", "state", "postcode", "country"):
            if address.get(k) is not None:
                addr_parts.append(f"<{k}>{_cdata(str(address.get(k)))}</{k}>")
        addr_parts.append("</address>")
        parts.append("".join(addr_parts))

    # permissiongroups (si se envía, REEMPLAZA según doc)
    if "permissiongroups" in p and p.get("permissiongroups") is not None:
        permissiongroups = p.get("permissiongroups")
        if not isinstance(permissiongroups, list):
            raise ValueError("permissiongroups debe ser lista de objetos {groupid: ...}.")
        pg_parts: List[str] = ["<permissiongroups>"]
        for g in permissiongroups:
            if not isinstance(g, dict):
                raise ValueError("permissiongroups debe ser lista de objetos.")
            gid = g.get("groupid")
            if gid is None or not str(gid).strip():
                raise ValueError("Cada permissiongroup requiere groupid.")
            pg_parts.append(f"<permissiongroup><groupid>{gid}</groupid></permissiongroup>")
        pg_parts.append("</permissiongroups>")
        parts.append("".join(pg_parts))

    # customfields (opcional)
    if "customfields" in p and p.get("customfields") is not None:
        customfields = p.get("customfields")
        if not isinstance(customfields, list):
            raise ValueError("customfields debe ser lista de objetos.")
        cf_parts: List[str] = ["<customfields>"]
        for cf in customfields:
            if not isinstance(cf, dict):
                raise ValueError("customfields debe ser lista de objetos.")
            cf_parts.append("<customfield>")
            if cf.get("fieldid") is not None:
                cf_parts.append(f"<fieldid>{cf['fieldid']}</fieldid>")
            if cf.get("name") is not None:
                cf_parts.append(f"<name>{_cdata(str(cf['name']))}</name>")
            if cf.get("type") is not None:
                cf_parts.append(f"<type>{_cdata(str(cf['type']))}</type>")
            if cf.get("value") is not None:
                cf_parts.append(f"<value>{_cdata(str(cf['value']))}</value>")
            cf_parts.append("</customfield>")
        cf_parts.append("</customfields>")
        parts.append("".join(cf_parts))

    # featureaccess (opcional)
    if "featureaccess" in p and p.get("featureaccess") is not None:
        featureaccess = p.get("featureaccess")
        if not isinstance(featureaccess, list):
            raise ValueError("featureaccess debe ser lista de objetos {featureid, featurevalue}.")
        fa_parts: List[str] = ["<featureaccess>"]
        for f in featureaccess:
            if not isinstance(f, dict):
                raise ValueError("featureaccess debe ser lista de objetos.")
            fid = f.get("featureid")
            fval = f.get("featurevalue")
            if fid is None or fval is None:
                raise ValueError("Cada featureaccess requiere featureid y featurevalue.")
            fa_parts.append("<feature>")
            fa_parts.append(f"<featureid>{fid}</featureid>")
            fa_parts.append(f"<featurevalue>{_cdata(str(fval))}</featurevalue>")
            fa_parts.append("</feature>")
        fa_parts.append("</featureaccess>")
        parts.append("".join(fa_parts))

    parts.append("</user></users>")
    return "".join(parts)


def _build_postxml_update_customfields(userid: str, customfields: List[Dict[str, Any]]) -> str:
    if not customfields:
        raise ValueError("customfields no puede estar vacío.")
    parts: List[str] = ["<users><user>", f"<userid>{userid}</userid>", "<customfields>"]
    for cf in customfields:
        if not isinstance(cf, dict):
            raise ValueError("customfields debe ser lista de objetos.")
        parts.append("<customfield>")
        if cf.get("fieldid") is not None:
            parts.append(f"<fieldid>{cf['fieldid']}</fieldid>")
        if cf.get("name") is not None:
            parts.append(f"<name>{_cdata(str(cf['name']))}</name>")
        if cf.get("type") is not None:
            parts.append(f"<type>{_cdata(str(cf['type']))}</type>")
        if cf.get("value") is not None:
            parts.append(f"<value>{_cdata(str(cf['value']))}</value>")
        parts.append("</customfield>")
    parts.append("</customfields></user></users>")
    return "".join(parts)


def _build_postxml_update_permissiongroups(userid: str, permissiongroups: List[Dict[str, Any]]) -> str:
    if not permissiongroups:
        raise ValueError("permissiongroups no puede estar vacío.")
    parts: List[str] = ["<users><user>", f"<userid>{userid}</userid>", "<permissiongroups>"]
    for g in permissiongroups:
        if not isinstance(g, dict):
            raise ValueError("permissiongroups debe ser lista de objetos {groupid: ...}.")
        gid = g.get("groupid")
        if gid is None or not str(gid).strip():
            raise ValueError("Cada permissiongroup requiere groupid.")
        parts.append(f"<permissiongroup><groupid>{gid}</groupid></permissiongroup>")
    parts.append("</permissiongroups></user></users>")
    return "".join(parts)


def _build_postxml_update_featureaccess(userid: str, featureaccess: List[Dict[str, Any]]) -> str:
    if not featureaccess:
        raise ValueError("featureaccess no puede estar vacío.")
    parts: List[str] = ["<users><user>", f"<userid>{userid}</userid>", "<featureaccess>"]
    for f in featureaccess:
        if not isinstance(f, dict):
            raise ValueError("featureaccess debe ser lista de objetos {featureid, featurevalue}.")
        fid = f.get("featureid")
        fval = f.get("featurevalue")
        if fid is None or fval is None:
            raise ValueError("Cada featureaccess requiere featureid y featurevalue.")
        parts.append("<feature>")
        parts.append(f"<featureid>{fid}</featureid>")
        parts.append(f"<featurevalue>{_cdata(str(fval))}</featurevalue>")
        parts.append("</feature>")
    parts.append("</featureaccess></user></users>")
    return "".join(parts)


# -------------------------
# Operations registry
# -------------------------
def get_operations() -> List[ZoneOperation]:
    return [
        ZoneOperation(
            code=OP_CREATE,
            label="Create User",
            description="Crea un nuevo user (POST zone=users) usando postxml.",
            http_method="POST",
            side_effect="write",
            idempotent=False,
            default_params={
                "raw": False,
                "dry_run": False,
                "givennames": None,
                "surname": None,
                "username": None,
                "password": None,
                "org": None,  # {"orgid": "..."}
                "email": None,
                "email2": None,
                "phone": None,
                "fax": None,
                "mobile": None,
                "position": None,
                "accesstype": None,
                "address": None,
                "permissiongroups": None,
                "customfields": None,
                "featureaccess": None,
            },
            params=[
                ParamSpec("givennames", "string", True, "Nombres."),
                ParamSpec("surname", "string", True, "Apellidos."),
                ParamSpec("username", "string", True, "Username/login."),
                ParamSpec("password", "string", True, "Password (solo CREATE)."),
                ParamSpec("org", "object", True, "Objeto org: {orgid}.", properties_schema={"orgid": {"type": "string"}}),
                ParamSpec("email", "string", False, "Email."),
                ParamSpec("email2", "string", False, "Email alterno."),
                ParamSpec("phone", "string", False, "Teléfono."),
                ParamSpec("fax", "string", False, "Fax."),
                ParamSpec("mobile", "string", False, "Móvil."),
                ParamSpec("position", "string", False, "Cargo/posición."),
                ParamSpec("accesstype", "string", False, "Tipo de acceso (si aplica)."),
                ParamSpec("address", "object", False, "Dirección (objeto)."),
                ParamSpec("permissiongroups", "array", False, "Lista [{groupid}].", items_schema={"type": "object"}),
                ParamSpec("customfields", "array", False, "Lista customfields.", items_schema={"type": "object"}),
                ParamSpec("featureaccess", "array", False, "Lista feature access.", items_schema={"type": "object"}),
                ParamSpec("dry_run", "boolean", False, "Si true, no ejecuta POST; devuelve preview (postxml/params)."),
                ParamSpec("raw", "boolean", False, "Si true, devuelve respuesta cruda + meta debug."),
            ],
            category="users",
            use_cases=["Crear usuario de prueba", "Provisionar usuario desde automatización"],
            risk_level="high",
            requires_confirmation=True,
        ),
        ZoneOperation(
            code=OP_UPDATE,
            label="Update User",
            description=(
                "Actualiza un user existente por userid (POST zone=users) usando postxml. "
                "ATENCIÓN: si envías permissiongroups, AroFlo REEMPLAZA los grupos existentes."
            ),
            http_method="POST",
            side_effect="write",
            idempotent=False,
            default_params={"raw": False, "dry_run": False},
            params=[
                ParamSpec("userid", "string", True, "UserID a actualizar."),
                ParamSpec("givennames", "string", False, "Nombres."),
                ParamSpec("surname", "string", False, "Apellidos."),
                ParamSpec("username", "string", False, "Username."),
                ParamSpec("email", "string", False, "Email."),
                ParamSpec("email2", "string", False, "Email alterno."),
                ParamSpec("phone", "string", False, "Teléfono."),
                ParamSpec("fax", "string", False, "Fax."),
                ParamSpec("mobile", "string", False, "Móvil."),
                ParamSpec("position", "string", False, "Cargo/posición."),
                ParamSpec("accesstype", "string", False, "Tipo de acceso (si aplica)."),
                ParamSpec("archived", "boolean", False, "Archivar/desarchivar usuario."),
                ParamSpec("org", "object", False, "Objeto org: {orgid}.", properties_schema={"orgid": {"type": "string"}}),
                ParamSpec("address", "object", False, "Dirección (objeto)."),
                ParamSpec("permissiongroups", "array", False, "Lista [{groupid}] - REEMPLAZA.", items_schema={"type": "object"}),
                ParamSpec("customfields", "array", False, "Lista customfields.", items_schema={"type": "object"}),
                ParamSpec("featureaccess", "array", False, "Lista feature access.", items_schema={"type": "object"}),
                ParamSpec("dry_run", "boolean", False, "Si true, no ejecuta POST; devuelve preview."),
                ParamSpec("raw", "boolean", False, "Debug raw."),
            ],
            category="users",
            use_cases=["Actualizar datos de usuario", "Archivar usuario", "Cambiar grupos (reemplaza)"],
            risk_level="high",
            requires_confirmation=True,
        ),
        ZoneOperation(
            code=OP_UPDATE_CUSTOMFIELDS,
            label="Update User Customfields",
            description="Actualiza customfields de un usuario (userid + customfields) usando postxml.",
            http_method="POST",
            side_effect="write",
            idempotent=False,
            default_params={"raw": False, "dry_run": False},
            params=[
                ParamSpec("userid", "string", True, "UserID destino."),
                ParamSpec("customfields", "array", True, "Lista customfields.", items_schema={"type": "object"}),
                ParamSpec("dry_run", "boolean", False, "Dry run."),
                ParamSpec("raw", "boolean", False, "Debug raw."),
            ],
            category="users",
            risk_level="medium",
            requires_confirmation=True,
        ),
        ZoneOperation(
            code=OP_UPDATE_PERMISSIONGROUPS,
            label="Update User Permission Groups (REPLACES)",
            description=(
                "Actualiza permissiongroups de un usuario (userid + permissiongroups) usando postxml. "
                "ATENCIÓN: REEMPLAZA (borra los existentes y deja los enviados)."
            ),
            http_method="POST",
            side_effect="write",
            idempotent=False,
            default_params={"raw": False, "dry_run": False},
            params=[
                ParamSpec("userid", "string", True, "UserID destino."),
                ParamSpec("permissiongroups", "array", True, "Lista [{groupid}] (REEMPLAZA).", items_schema={"type": "object"}),
                ParamSpec("dry_run", "boolean", False, "Dry run."),
                ParamSpec("raw", "boolean", False, "Debug raw."),
            ],
            category="users",
            risk_level="high",
            requires_confirmation=True,
        ),
        ZoneOperation(
            code=OP_UPDATE_FEATUREACCESS,
            label="Update User Feature Access",
            description="Actualiza featureaccess de un usuario (userid + featureaccess) usando postxml.",
            http_method="POST",
            side_effect="write",
            idempotent=False,
            default_params={"raw": False, "dry_run": False},
            params=[
                ParamSpec("userid", "string", True, "UserID destino."),
                ParamSpec("featureaccess", "array", True, "Lista [{featureid, featurevalue}].", items_schema={"type": "object"}),
                ParamSpec("dry_run", "boolean", False, "Dry run."),
                ParamSpec("raw", "boolean", False, "Debug raw."),
            ],
            category="users",
            risk_level="medium",
            requires_confirmation=True,
        ),

        ZoneOperation(
            code=OP_UI_UPLOAD_DOCS,
            label="UI Upload User Documents",
            description="Abre usuario por email en UI y sube N documentos con comment/filter.",
            http_method="UI",
            side_effect="write",
            idempotent=False,
            default_params={
                "user_email": None,
                "timesheet_bu": None,  # si tu flujo requiere BU selector antes
                "docs": None,          # List[dict]
                "dry_run": False,
                "raw": False,
            },
            params=[
                ParamSpec("user_email", "string", True, "Email del usuario en AroFlo."),
                ParamSpec("timesheet_bu", "string", False, "Business Unit (si aplica para tu sesión/UI)."),
                ParamSpec(
                    "docs", "array", True,
                    "Lista de documentos: [{file, comment?, filter?}]",
                    items_schema={"type": "object"},
                ),
                ParamSpec("dry_run", "boolean", False, "Si true, no ejecuta UI; solo preview del comando."),
                ParamSpec("raw", "boolean", False, "Si true, incluye stdout/stderr completos del runner."),
            ],
            category="users",
            use_cases=["Adjuntar certificados médicos", "Automatizar documentos del Leave Form"],
            risk_level="high",
            requires_confirmation=True,
        ),


    ]


def supports(operation_code: str) -> bool:
    return operation_code in {
        OP_CREATE,
        OP_UPDATE,
        OP_UPDATE_CUSTOMFIELDS,
        OP_UPDATE_PERMISSIONGROUPS,
        OP_UPDATE_FEATUREACCESS,
        OP_UI_UPLOAD_DOCS,
    }

def _run_ui(argv: List[str], *, raw: bool) -> Any:
    proc = subprocess.run(argv, capture_output=True, text=True, check=False)
    if raw:
        return {"returncode": proc.returncode, "stdout": proc.stdout, "stderr": proc.stderr, "argv": argv}
    out_tail = (proc.stdout or "").strip().splitlines()[-30:]
    err_tail = (proc.stderr or "").strip().splitlines()[-30:]
    return {"returncode": proc.returncode, "stdout_tail": "\n".join(out_tail), "stderr_tail": "\n".join(err_tail), "argv": argv}



def execute(operation_code: str, client: Any, params: Dict[str, Any]) -> Any:
    raw = bool(params.get("raw", False))
    dry_run = bool(params.get("dry_run", False))

    def _preview(params_list: List[Tuple[str, str]], postxml: str) -> Dict[str, Any]:
        return {"dry_run": True, "http_method": "POST", "params": params_list, "postxml": postxml}

    if operation_code == OP_CREATE:
        postxml = _build_postxml_create(params)
        params_list: List[Tuple[str, str]] = [("zone", AF_ZONE_USERS), ("postxml", postxml)]
        if dry_run:
            return _preview(params_list, postxml)
        resp = request(client, "POST", params_list)
        return raw_wrap(resp, params_list) if raw else resp

    if operation_code == OP_UPDATE:
        postxml = _build_postxml_update(params)
        params_list = [("zone", AF_ZONE_USERS), ("postxml", postxml)]
        if dry_run:
            return _preview(params_list, postxml)
        resp = request(client, "POST", params_list)
        return raw_wrap(resp, params_list) if raw else resp

    if operation_code == OP_UPDATE_CUSTOMFIELDS:
        userid = _require_non_empty_str(params, "userid")
        customfields = params.get("customfields")
        if not isinstance(customfields, list):
            raise ValueError("customfields debe ser una lista.")
        postxml = _build_postxml_update_customfields(userid, customfields)
        params_list = [("zone", AF_ZONE_USERS), ("postxml", postxml)]
        if dry_run:
            return _preview(params_list, postxml)
        resp = request(client, "POST", params_list)
        return raw_wrap(resp, params_list) if raw else resp

    if operation_code == OP_UPDATE_PERMISSIONGROUPS:
        userid = _require_non_empty_str(params, "userid")
        permissiongroups = params.get("permissiongroups")
        if not isinstance(permissiongroups, list):
            raise ValueError("permissiongroups debe ser una lista.")
        postxml = _build_postxml_update_permissiongroups(userid, permissiongroups)
        params_list = [("zone", AF_ZONE_USERS), ("postxml", postxml)]
        if dry_run:
            return _preview(params_list, postxml)
        resp = request(client, "POST", params_list)
        return raw_wrap(resp, params_list) if raw else resp

    if operation_code == OP_UPDATE_FEATUREACCESS:
        userid = _require_non_empty_str(params, "userid")
        featureaccess = params.get("featureaccess")
        if not isinstance(featureaccess, list):
            raise ValueError("featureaccess debe ser una lista.")
        postxml = _build_postxml_update_featureaccess(userid, featureaccess)
        params_list = [("zone", AF_ZONE_USERS), ("postxml", postxml)]
        if dry_run:
            return _preview(params_list, postxml)
        resp = request(client, "POST", params_list)
        return raw_wrap(resp, params_list) if raw else resp

    if operation_code == OP_UI_UPLOAD_DOCS:
        user_email = str(params.get("user_email") or params.get("user-email") or "").strip()
        if not user_email:
            raise ValueError("user_email es requerido (ej: cpenuela@usg.com.au).")

        docs = params.get("docs") or []
        if not isinstance(docs, list) or not docs:
            raise ValueError("docs es requerido y debe ser una lista no vacía de strings --doc.")

        timesheet_bu = str(params.get("timesheet_bu") or params.get("timesheet-bu") or "").strip()

        argv: List[str] = [
            sys.executable,
            "-m",
            "apps.aroflo_connector_app.ui_automation.runner",
        ]

        # BU (recomendado si el runner entra por Manage -> Users con BU aplicada)
        if timesheet_bu:
            argv.extend(["--timesheet-bu", timesheet_bu])

        # Identificador principal para Users
        argv.extend(["--user-email", user_email])

        # Tripletas --doc (strings)
        for d in docs:
            s = str(d).strip()
            if not s:
                continue
            # opcional: validación suave de que contenga "file="
            if "file=" not in s:
                raise ValueError(f"Cada doc debe incluir 'file=...'. Recibido: {s}")
            argv.extend(["--doc", s])

        argv.append("users-upload-docs")

        if dry_run:
            return {"dry_run": True, "invocation": "subprocess", "argv": argv}

        return _run_ui(argv, raw=raw)

    raise ValueError(f"[Users.mutations] Operación no soportada: {operation_code}")
