# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Typed Port System for the Graphical Editor.

Defines PortSchema, PORT_TYPE_CATALOG, SYSTEM_VARIABLES,
output normalizers, input extractors, and Transit helpers.
"""

import logging
import time
import uuid
from typing import Any, Callable, Dict, List, Optional

from pydantic import BaseModel, Field

from modules.shared.i18nRegistry import resolveText

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Pydantic models
# ---------------------------------------------------------------------------

class PortField(BaseModel):
    name: str
    type: str  # str, int, bool, List[str], List[Document], Dict[str,Any]
    description: str = ""
    required: bool = True


class PortSchema(BaseModel):
    name: str  # e.g. "EmailDraft", "AiResult", "Transit"
    fields: List[PortField]


class InputPortDef(BaseModel):
    accepts: List[str]  # list of accepted schema names


class OutputPortDef(BaseModel):
    model_config = {"populate_by_name": True}

    schema_: str = Field(alias="schema")
    dynamic: bool = False
    deriveFrom: Optional[str] = None

    def model_dump(self, **kw):
        d = super().model_dump(**kw)
        d["schema"] = d.pop("schema_", d.get("schema"))
        return d


# ---------------------------------------------------------------------------
# PORT_TYPE_CATALOG
# ---------------------------------------------------------------------------

PORT_TYPE_CATALOG: Dict[str, PortSchema] = {
    "DocumentList": PortSchema(name="DocumentList", fields=[
        PortField(name="documents", type="List[Document]",
                  description="Dokumentenliste"),
    ]),
    "FileList": PortSchema(name="FileList", fields=[
        PortField(name="files", type="List[File]",
                  description="Dateiliste"),
    ]),
    "EmailDraft": PortSchema(name="EmailDraft", fields=[
        PortField(name="subject", type="str",
                  description="Betreff"),
        PortField(name="body", type="str",
                  description="Inhalt"),
        PortField(name="to", type="List[str]",
                  description="Empfänger"),
        PortField(name="cc", type="List[str]", required=False,
                  description="CC"),
        PortField(name="attachments", type="List[Document]", required=False,
                  description="Anhänge"),
    ]),
    "EmailList": PortSchema(name="EmailList", fields=[
        PortField(name="emails", type="List[Email]",
                  description="E-Mails"),
    ]),
    "TaskList": PortSchema(name="TaskList", fields=[
        PortField(name="tasks", type="List[Task]",
                  description="Aufgaben"),
    ]),
    "TaskResult": PortSchema(name="TaskResult", fields=[
        PortField(name="success", type="bool",
                  description="Erfolg"),
        PortField(name="taskId", type="str",
                  description="Aufgaben-ID"),
        PortField(name="task", type="Dict",
                  description="Aufgabendaten"),
    ]),
    "FormPayload": PortSchema(name="FormPayload", fields=[
        PortField(name="payload", type="Dict[str,Any]",
                  description="Formulardaten"),
    ]),
    "AiResult": PortSchema(name="AiResult", fields=[
        PortField(name="prompt", type="str",
                  description="Prompt"),
        PortField(name="response", type="str",
                  description="Antworttext"),
        PortField(name="responseData", type="Dict", required=False,
                  description="Strukturierte Antwort"),
        PortField(name="context", type="str",
                  description="Kontext"),
        PortField(name="documents", type="List[Document]",
                  description="Dokumente"),
    ]),
    "BoolResult": PortSchema(name="BoolResult", fields=[
        PortField(name="result", type="bool",
                  description="Ergebnis"),
        PortField(name="reason", type="str", required=False,
                  description="Begründung"),
    ]),
    "TextResult": PortSchema(name="TextResult", fields=[
        PortField(name="text", type="str",
                  description="Text"),
    ]),
    "LoopItem": PortSchema(name="LoopItem", fields=[
        PortField(name="currentItem", type="Any",
                  description="Aktuelles Element"),
        PortField(name="currentIndex", type="int",
                  description="Aktueller Index"),
        PortField(name="items", type="List[Any]",
                  description="Alle Elemente"),
        PortField(name="count", type="int",
                  description="Gesamtanzahl"),
    ]),
    "AggregateResult": PortSchema(name="AggregateResult", fields=[
        PortField(name="items", type="List[Any]",
                  description="Gesammelte Elemente"),
        PortField(name="count", type="int",
                  description="Anzahl"),
    ]),
    "MergeResult": PortSchema(name="MergeResult", fields=[
        PortField(name="inputs", type="Dict[int,Any]",
                  description="Eingaben nach Port"),
        PortField(name="first", type="Any",
                  description="Erstes verfügbares"),
        PortField(name="merged", type="Dict",
                  description="Zusammengeführte Daten"),
    ]),
    "ActionResult": PortSchema(name="ActionResult", fields=[
        PortField(name="success", type="bool",
                  description="Erfolg"),
        PortField(name="error", type="str", required=False,
                  description="Fehler"),
        PortField(name="data", type="Dict", required=False,
                  description="Ergebnisdaten"),
    ]),
    "Transit": PortSchema(name="Transit", fields=[]),
}


# ---------------------------------------------------------------------------
# SYSTEM_VARIABLES
# ---------------------------------------------------------------------------

SYSTEM_VARIABLES: Dict[str, Dict[str, str]] = {
    "system.timestamp":   {"type": "int",  "description": "Unix timestamp (ms)"},
    "system.date":        {"type": "str",  "description": "ISO date (YYYY-MM-DD)"},
    "system.datetime":    {"type": "str",  "description": "ISO datetime"},
    "system.time":        {"type": "str",  "description": "HH:MM:SS"},
    "system.userId":      {"type": "str",  "description": "Current user ID"},
    "system.userName":    {"type": "str",  "description": "Current user name"},
    "system.userEmail":   {"type": "str",  "description": "Current user email"},
    "system.workflowId":  {"type": "str",  "description": "Workflow ID"},
    "system.runId":       {"type": "str",  "description": "Run ID"},
    "system.instanceId":  {"type": "str",  "description": "Feature instance ID"},
    "system.mandateId":   {"type": "str",  "description": "Mandate ID"},
    "system.loopIndex":   {"type": "int",  "description": "Current loop index (only in loop)"},
    "system.loopCount":   {"type": "int",  "description": "Loop item count (only in loop)"},
    "system.uuid":        {"type": "str",  "description": "Random UUID"},
}


def _resolveSystemVariable(variable: str, context: Dict[str, Any]) -> Any:
    """Resolve a system variable name to its runtime value."""
    from datetime import datetime, timezone

    now = datetime.now(timezone.utc)
    mapping = {
        "system.timestamp": lambda: int(now.timestamp() * 1000),
        "system.date": lambda: now.strftime("%Y-%m-%d"),
        "system.datetime": lambda: now.isoformat(),
        "system.time": lambda: now.strftime("%H:%M:%S"),
        "system.userId": lambda: context.get("userId", ""),
        "system.userName": lambda: context.get("userName", ""),
        "system.userEmail": lambda: context.get("userEmail", ""),
        "system.workflowId": lambda: context.get("workflowId", ""),
        "system.runId": lambda: context.get("_runId", ""),
        "system.instanceId": lambda: context.get("instanceId", ""),
        "system.mandateId": lambda: context.get("mandateId", ""),
        "system.loopIndex": lambda: (context.get("_loopState") or {}).get("currentIndex", -1),
        "system.loopCount": lambda: len((context.get("_loopState") or {}).get("items", [])),
        "system.uuid": lambda: str(uuid.uuid4()),
    }
    resolver = mapping.get(variable)
    if resolver:
        return resolver()
    logger.warning("Unknown system variable: %s", variable)
    return None


# ---------------------------------------------------------------------------
# Output normalizers
# ---------------------------------------------------------------------------

def _normalizeToSchema(raw: Any, schemaName: str) -> Dict[str, Any]:
    """
    Normalize raw executor output to match the declared port schema.
    Ensures _success/_error meta-fields are always present.
    """
    if not isinstance(raw, dict):
        raw = {"value": raw} if raw is not None else {}

    result = dict(raw)
    result.setdefault("_success", not bool(raw.get("error")))
    result.setdefault("_error", raw.get("error"))

    schema = PORT_TYPE_CATALOG.get(schemaName)
    if not schema or schemaName == "Transit":
        return result

    for field in schema.fields:
        if field.name not in result:
            result[field.name] = _defaultForType(field.type)

    return result


def _defaultForType(typeStr: str) -> Any:
    """Return a sensible default for a type string."""
    if typeStr.startswith("List"):
        return []
    if typeStr.startswith("Dict"):
        return {}
    if typeStr == "bool":
        return False
    if typeStr == "int":
        return 0
    if typeStr == "str":
        return ""
    return None


def _normalizeError(error: Exception, schemaName: str) -> Dict[str, Any]:
    """Build an error envelope matching the schema with _success=False."""
    result = {"_success": False, "_error": str(error)}
    schema = PORT_TYPE_CATALOG.get(schemaName)
    if schema:
        for field in schema.fields:
            result.setdefault(field.name, _defaultForType(field.type))
    return result


# ---------------------------------------------------------------------------
# Input extractors (one per input port type)
# ---------------------------------------------------------------------------

def _extractEmailDraft(upstream: Dict[str, Any]) -> Dict[str, Any]:
    """Extract EmailDraft fields from upstream output."""
    result = {}
    if upstream.get("responseData") and isinstance(upstream["responseData"], dict):
        rd = upstream["responseData"]
        for key in ("subject", "body", "to", "cc"):
            if key in rd:
                result[key] = rd[key]
    if not result:
        for key in ("subject", "body", "to", "cc"):
            if key in upstream:
                result[key] = upstream[key]
    return result


def _extractDocuments(upstream: Dict[str, Any]) -> Dict[str, Any]:
    """Extract documents from upstream output."""
    docs = upstream.get("documents") or upstream.get("documentList") or []
    if not docs and isinstance(upstream.get("data"), dict):
        docs = upstream["data"].get("documents") or upstream["data"].get("documentList") or []
    # input.upload format
    if not docs:
        files = upstream.get("files") or []
        fileObj = upstream.get("file")
        fileIds = upstream.get("fileIds") or []
        if fileObj:
            docs = [fileObj]
        elif files:
            docs = files
        elif fileIds:
            docs = [{"validationMetadata": {"fileId": fid}} for fid in fileIds]
    normalized = docs if isinstance(docs, list) else [docs]
    return {"documents": normalized, "documentList": normalized} if docs else {}


def _extractText(upstream: Dict[str, Any]) -> Dict[str, Any]:
    """Extract text from upstream output."""
    text = upstream.get("text") or upstream.get("response") or upstream.get("context") or ""
    if not text and upstream.get("payload"):
        import json
        payload = upstream["payload"]
        text = json.dumps(payload, ensure_ascii=False) if isinstance(payload, dict) else str(payload)
    return {"text": str(text)} if text else {}


def _extractEmailList(upstream: Dict[str, Any]) -> Dict[str, Any]:
    """Extract email list from upstream output."""
    emails = upstream.get("emails") or []
    if not emails:
        docs = upstream.get("documents") or upstream.get("documentList") or []
        if docs:
            import json
            for doc in docs:
                raw = doc.get("documentData") if isinstance(doc, dict) else None
                if raw:
                    try:
                        data = json.loads(raw) if isinstance(raw, str) else raw
                        if isinstance(data, dict):
                            found = (data.get("emails", {}).get("emails", [])
                                     or data.get("searchResults", {}).get("results", []))
                            if found:
                                emails = found
                                break
                    except (json.JSONDecodeError, TypeError):
                        pass
    return {"emails": emails} if emails else {}


def _extractTaskList(upstream: Dict[str, Any]) -> Dict[str, Any]:
    """Extract task list from upstream output."""
    tasks = upstream.get("tasks") or []
    if not tasks:
        docs = upstream.get("documents") or upstream.get("documentList") or []
        if docs:
            import json
            for doc in docs:
                raw = doc.get("documentData") if isinstance(doc, dict) else None
                if raw:
                    try:
                        data = json.loads(raw) if isinstance(raw, str) else raw
                        if isinstance(data, dict) and "tasks" in data:
                            tasks = data["tasks"]
                            break
                    except (json.JSONDecodeError, TypeError):
                        pass
    return {"tasks": tasks} if tasks else {}


def _extractFileList(upstream: Dict[str, Any]) -> Dict[str, Any]:
    """Extract file list from upstream output."""
    files = upstream.get("files") or []
    return {"files": files} if files else {}


def _extractFormPayload(upstream: Dict[str, Any]) -> Dict[str, Any]:
    """Extract form payload from upstream output."""
    payload = upstream.get("payload")
    if payload and isinstance(payload, dict):
        return {"payload": payload}
    return {}


def _extractAiResult(upstream: Dict[str, Any]) -> Dict[str, Any]:
    """Extract AI result fields from upstream output."""
    result = {}
    for key in ("prompt", "response", "responseData", "context", "documents"):
        if key in upstream:
            result[key] = upstream[key]
    return result


def _extractBoolResult(upstream: Dict[str, Any]) -> Dict[str, Any]:
    """Extract bool result from upstream output."""
    result = upstream.get("result")
    if isinstance(result, bool):
        return {"result": result, "reason": upstream.get("reason", "")}
    approved = upstream.get("approved")
    if isinstance(approved, bool):
        return {"result": approved, "reason": upstream.get("reason", "")}
    return {}


def _extractTaskResult(upstream: Dict[str, Any]) -> Dict[str, Any]:
    """Extract task result from upstream output."""
    result = {}
    if "taskId" in upstream:
        result["taskId"] = upstream["taskId"]
    if "task" in upstream:
        result["task"] = upstream["task"]
    elif "clickupTask" in upstream:
        result["task"] = upstream["clickupTask"]
    if "success" in upstream:
        result["success"] = upstream["success"]
    return result


def _extractAggregateResult(upstream: Dict[str, Any]) -> Dict[str, Any]:
    """Extract aggregate result from upstream output."""
    items = upstream.get("items") or []
    return {"items": items, "count": len(items)}


def _extractMergeResult(upstream: Dict[str, Any]) -> Dict[str, Any]:
    """Extract merge result from upstream output."""
    return {
        "inputs": upstream.get("inputs", {}),
        "first": upstream.get("first"),
        "merged": upstream.get("merged", {}),
    }


INPUT_EXTRACTORS: Dict[str, Callable] = {
    "EmailDraft": _extractEmailDraft,
    "DocumentList": _extractDocuments,
    "TextResult": _extractText,
    "EmailList": _extractEmailList,
    "TaskList": _extractTaskList,
    "FileList": _extractFileList,
    "FormPayload": _extractFormPayload,
    "AiResult": _extractAiResult,
    "BoolResult": _extractBoolResult,
    "TaskResult": _extractTaskResult,
    "AggregateResult": _extractAggregateResult,
    "MergeResult": _extractMergeResult,
}


# ---------------------------------------------------------------------------
# Transit helpers
# ---------------------------------------------------------------------------

def _wrapTransit(data: Any, meta: Dict[str, Any]) -> Dict[str, Any]:
    """Wrap data in a Transit envelope."""
    return {"_transit": True, "_meta": meta, "data": data}


def _unwrapTransit(output: Any) -> Any:
    """Unwrap a Transit envelope, returning the inner data."""
    if isinstance(output, dict) and output.get("_transit"):
        return output.get("data")
    return output


def _resolveTransitChain(
    nodeId: str,
    nodeOutputs: Dict[str, Any],
    connectionMap: Dict[str, list],
) -> Any:
    """
    Follow _transit chain backwards until a real (non-transit) producer is found.
    Returns the unwrapped output of the real producer.
    """
    visited = set()
    current = nodeId
    while current and current not in visited:
        visited.add(current)
        out = nodeOutputs.get(current)
        if not isinstance(out, dict) or not out.get("_transit"):
            return out
        sources = connectionMap.get(current, [])
        if not sources:
            return _unwrapTransit(out)
        srcId = sources[0][0] if sources else None
        if not srcId:
            return _unwrapTransit(out)
        current = srcId
    return nodeOutputs.get(nodeId)


# ---------------------------------------------------------------------------
# Schema derivation for dynamic outputs
# ---------------------------------------------------------------------------

def _deriveFormPayloadSchema(node: Dict[str, Any]) -> Optional[PortSchema]:
    """Derive output schema from form field definitions."""
    fields_param = (node.get("parameters") or {}).get("fields")
    if not fields_param or not isinstance(fields_param, list):
        return None
    portFields = []
    for f in fields_param:
        if isinstance(f, dict) and f.get("name"):
            _lab = f.get("label")
            _desc = resolveText(_lab) if _lab is not None else f["name"]
            if not _desc.strip():
                _desc = f["name"]
            portFields.append(PortField(
                name=f["name"],
                type=f.get("type", "str"),
                description=_desc,
                required=f.get("required", False),
            ))
    return PortSchema(name="FormPayload_dynamic", fields=portFields) if portFields else None


def _deriveTransformSchema(node: Dict[str, Any]) -> Optional[PortSchema]:
    """Derive output schema from transform mappings."""
    mappings = (node.get("parameters") or {}).get("mappings")
    if not mappings or not isinstance(mappings, list):
        return None
    portFields = []
    for m in mappings:
        if isinstance(m, dict) and m.get("outputField"):
            portFields.append(PortField(
                name=m["outputField"],
                type=m.get("type", "str"),
                description=str(m.get("label", m["outputField"])),
            ))
    return PortSchema(name="Transform_dynamic", fields=portFields) if portFields else None
