/**
 * DSL Workflow — The dynamic interpreter that executes any workflow DSL.
 *
 * This is the ONLY Temporal workflow in the system. It receives a DSL definition
 * and interprets it at runtime, calling the appropriate activities for each node.
 *
 * Temporal Workflow constraints apply:
 * - Must be deterministic (no Math.random, Date.now, etc.)
 * - Side effects only through activities
 * - Use workflow.sleep() instead of setTimeout
 */
import {
  proxyActivities,
  defineSignal,
  defineQuery,
  setHandler,
  condition,
  sleep,
  CancellationScope,
  isCancellation,
  executeChild,
} from '@temporalio/workflow';
import type {
  WorkflowInput,
  WorkflowResult,
  DslNode,
  WorkflowDSL,
  NodeStatus,
  ApprovalSignalPayload,
  ReportNodeStatusInput,
  ReportWorkflowStatusInput,
} from '../dsl/types';
import { assertNever } from '../dsl/node-types';
import {
  resolveTemplates,
  resolveExpression,
  getNestedValue,
  evaluateConditions,
  getSuccessorsFromNodes,
  getNextNodes,
} from '../dsl/helpers';

// ─── Activity Proxies ─────────────────────────────────────────────────────────
//
// Activity 按"职责类别"分组配置 timeout / retry，避免一刀切。
// startToCloseTimeout 必须覆盖：业务上限（含 axios/vm 内部 timeout）+ Worker SIGTERM 后 cancel 响应窗口。
// 业务可在 dsl.config 里通过 `timeoutSec` 覆盖某次调用，这里的值是默认值/上限。
//
// 注意：单条 Activity 的最坏耗时 ≈ startToCloseTimeout × maximumAttempts。
// 若长于 K8s Pod 的 grace period（300s）会被 SIGKILL，把后续重试推给其它 worker。

// HTTP — axios 默认 timeout 30s，给 Activity 预留 cancel 响应余量
const httpActs = proxyActivities<
  Pick<typeof import('../activities'), 'httpRequest'>
>({
  startToCloseTimeout: '60s',
  retry: { maximumAttempts: 3, initialInterval: '1s', maximumInterval: '30s' },
});

// DB — 慢查询 / 大写入可能比较久；不重试写入（dbExecute 可能非幂等）
const dbActs = proxyActivities<
  Pick<typeof import('../activities'), 'dbQuery' | 'dbExecute'>
>({
  startToCloseTimeout: '120s',
  retry: { maximumAttempts: 1 }, // 写入默认不重试，避免重复扣款类问题
});

// 通知 —— 邮件/IM 网关一般快；失败重试要积极
const notifyActs = proxyActivities<
  Pick<typeof import('../activities'), 'sendNotification'>
>({
  startToCloseTimeout: '30s',
  retry: { maximumAttempts: 5, initialInterval: '2s', maximumInterval: '60s' },
});

// 自定义 JS —— vm 内部默认 5s timeout，外层留足边界 + 不重试（脚本可能非幂等）
const codeActs = proxyActivities<
  Pick<typeof import('../activities'), 'codeExecute'>
>({
  startToCloseTimeout: '15s',
  retry: { maximumAttempts: 1 },
});

const reportActs = proxyActivities<typeof import('../activities')>({
  startToCloseTimeout: '10s',
  retry: { maximumAttempts: 5 },
});

// ─── Signals & Queries ────────────────────────────────────────────────────────

export const approvalSignal = defineSignal<[ApprovalSignalPayload]>('approval');
export const cancelSignal = defineSignal('cancel');

export const getProgressQuery = defineQuery<Record<string, NodeStatus>>('getProgress');

// ─── Main Workflow ────────────────────────────────────────────────────────────

export async function dslWorkflow(input: WorkflowInput): Promise<WorkflowResult> {
  const { instanceId, dsl, context: initialContext } = input;

  // Node status tracking (for queries)
  const nodeStatuses: Record<string, NodeStatus> = {};
  // Execution context — flows between nodes
  const ctx: Record<string, any> = { ...initialContext };
  // Approval results collected from signals
  const approvalResults: Record<string, ApprovalSignalPayload> = {};
  // Cancellation flag
  let cancelled = false;

  // Set up signal handlers
  setHandler(approvalSignal, (payload: ApprovalSignalPayload) => {
    approvalResults[payload.nodeId] = payload;
  });

  setHandler(cancelSignal, () => {
    cancelled = true;
  });

  setHandler(getProgressQuery, () => nodeStatuses);

  // Report workflow as RUNNING
  await reportActs.reportWorkflowStatus({ instanceId, status: 'RUNNING' });

  try {
    // Find trigger nodes and start from their successors
    const triggerNodes = dsl.nodes.filter((n) => n.type.endsWith('_trigger'));
    const startNodeIds = getSuccessorsFromNodes(dsl, triggerNodes.map((n) => n.id));

    // Execute the graph starting from trigger successors
    await executeGraph(dsl, startNodeIds, ctx, nodeStatuses, instanceId, approvalResults, () => cancelled);

    // Report success
    await reportActs.reportWorkflowStatus({ instanceId, status: 'COMPLETED', result: ctx });
    return { status: 'COMPLETED', context: ctx };
  } catch (err: any) {
    if (isCancellation(err) || cancelled) {
      await reportActs.reportWorkflowStatus({ instanceId, status: 'CANCELLED' });
      return { status: 'CANCELLED', context: ctx };
    }
    await reportActs.reportWorkflowStatus({ instanceId, status: 'FAILED', result: { error: err.message } });
    return { status: 'FAILED', context: ctx, error: err.message };
  }
}

// ─── Graph Execution Engine ───────────────────────────────────────────────────

async function executeGraph(
  dsl: WorkflowDSL,
  startNodeIds: string[],
  ctx: Record<string, any>,
  nodeStatuses: Record<string, NodeStatus>,
  instanceId: number,
  approvalResults: Record<string, ApprovalSignalPayload>,
  isCancelled: () => boolean,
): Promise<void> {
  // BFS-style execution following edges
  const executed = new Set<string>();
  const queue = [...startNodeIds];

  // 防御：DSL 是循环图（A→B→A）或不可达 predecessor 时，
  // 当前 BFS 会把 nodeId 不停 push 回 queue 形成死循环。
  // 给一个上限：每个节点最多被处理 nodeCount * 2 次（包含一次重排队）。
  const MAX_ITERATIONS = Math.max(dsl.nodes.length * 10, 100);
  let iterations = 0;

  while (queue.length > 0) {
    if (isCancelled()) throw new CancellationScope().cancel;

    if (++iterations > MAX_ITERATIONS) {
      throw new Error(
        `executeGraph exceeded ${MAX_ITERATIONS} iterations — DSL likely contains a cycle ` +
          `or unreachable predecessors. Pending queue: [${queue.join(', ')}], executed: [${[...executed].join(', ')}]`,
      );
    }

    const nodeId = queue.shift()!;
    if (executed.has(nodeId)) continue;

    // Check if all predecessors have been executed
    const predecessors = dsl.edges.filter((e) => e.target === nodeId).map((e) => e.source);
    const allPredsDone = predecessors.every((p) => executed.has(p) || dsl.nodes.find((n) => n.id === p)?.type.endsWith('_trigger'));
    if (!allPredsDone) {
      // Re-queue for later
      queue.push(nodeId);
      continue;
    }

    const node = dsl.nodes.find((n) => n.id === nodeId);
    if (!node) continue;

    executed.add(nodeId);
    nodeStatuses[nodeId] = 'RUNNING';

    // Report node as RUNNING
    await reportActs.reportNodeStatus({
      instanceId,
      nodeId: node.id,
      nodeType: node.type,
      nodeLabel: node.label,
      status: 'RUNNING',
      input: ctx,
    });

    try {
      // Execute the node and get result
      const result = await executeNode(node, dsl, ctx, instanceId, approvalResults, nodeStatuses, isCancelled);
      nodeStatuses[nodeId] = 'COMPLETED';

      // Report node completion
      await reportActs.reportNodeStatus({
        instanceId,
        nodeId: node.id,
        nodeType: node.type,
        nodeLabel: node.label,
        status: 'COMPLETED',
        output: result?.output,
      });

      // Merge output into context
      if (result?.output) {
        ctx[nodeId] = result.output;
      }

      // Determine next nodes based on node type
      const nextNodeIds = getNextNodes(dsl, node, result?.branch);
      for (const nextId of nextNodeIds) {
        if (!executed.has(nextId)) queue.push(nextId);
      }
    } catch (err: any) {
      nodeStatuses[nodeId] = 'FAILED';
      await reportActs.reportNodeStatus({
        instanceId,
        nodeId: node.id,
        nodeType: node.type,
        nodeLabel: node.label,
        status: 'FAILED',
        error: { message: err.message, stack: err.stack },
      });
      throw err; // Propagate to workflow level
    }
  }
}

// ─── Node Execution Dispatcher ────────────────────────────────────────────────

async function executeNode(
  node: DslNode,
  dsl: WorkflowDSL,
  ctx: Record<string, any>,
  instanceId: number,
  approvalResults: Record<string, ApprovalSignalPayload>,
  nodeStatuses: Record<string, NodeStatus>,
  isCancelled: () => boolean,
): Promise<{ output?: Record<string, any>; branch?: string } | undefined> {
  const config = resolveTemplates(node.config, ctx);

  switch (node.type) {
    // ─── Triggers (never reach executeNode; handled at graph entry) ──
    case 'cron_trigger':
    case 'webhook_trigger':
    case 'event_trigger':
    case 'manual_trigger':
      return { output: { triggeredAt: new Date().toISOString() } };

    // ─── Actions ────────────────────────────────────────────────────
    case 'http_request': {
      const result = await httpActs.httpRequest(config as any);
      return { output: result };
    }

    case 'db_query': {
      const result = await dbActs.dbQuery(config as any);
      return { output: result };
    }

    case 'db_execute': {
      const result = await dbActs.dbExecute(config as any);
      return { output: result };
    }

    case 'send_notification': {
      const result = await notifyActs.sendNotification(config as any);
      return { output: result };
    }

    case 'code': {
      const result = await codeActs.codeExecute({
        ...config,
        data: ctx,
      } as any);
      return { output: result };
    }

    // ─── Data Processing ────────────────────────────────────────────
    case 'set_variable': {
      const assignments = config.assignments as Array<{ name: string; value: string }>;
      const output: Record<string, any> = {};
      for (const { name, value } of assignments || []) {
        const resolved = resolveExpression(value, ctx);
        ctx[name] = resolved;
        output[name] = resolved;
      }
      return { output };
    }

    // ─── Conditions ─────────────────────────────────────────────────
    case 'if_else': {
      const conditions = config.conditions as Array<{ field: string; operator: string; value: string }> || [];
      const combineOp = (config.combineOperation as 'AND' | 'OR') || 'AND';
      const result = evaluateConditions(conditions, ctx, combineOp);
      return { output: { branch: result ? 'true' : 'false' }, branch: result ? 'true' : 'false' };
    }

    case 'switch': {
      const field = config.field as string;
      const cases = config.cases as Array<{ value: string; label: string }> || [];
      const fieldValue = String(getNestedValue(ctx, field) ?? '');
      const matched = cases.find((c) => c.value === fieldValue);
      const branch = matched ? matched.value : (config.defaultCase as string || 'default');
      return { output: { matchedCase: branch }, branch };
    }

    // ─── Flow Control ───────────────────────────────────────────────
    case 'delay': {
      const delayType = config.delayType as string;
      if (delayType === 'fixedDuration') {
        const duration = (config.duration as number) || 1;
        await sleep(duration * 1000);
      } else if (delayType === 'untilTime') {
        const targetTime = new Date(config.untilTime as string).getTime();
        const now = Date.now();
        if (targetTime > now) {
          await sleep(targetTime - now);
        }
      } else if (delayType === 'untilSignal') {
        // Wait for a specific signal (reuse approval mechanism)
        await condition(() => approvalResults[node.id] !== undefined, '24h');
      }
      return { output: { resumedAt: new Date().toISOString(), resumeReason: delayType } };
    }

    case 'parallel': {
      // Execute all outgoing branches in parallel
      const branches = getSuccessorsFromNodes(dsl, [node.id]);
      const branchPromises = branches.map(async (branchNodeId) => {
        await executeGraph(
          dsl,
          [branchNodeId],
          ctx,
          nodeStatuses,
          instanceId,
          approvalResults,
          isCancelled,
        );
      });

      const waitMode = config.waitMode as string || 'all';
      if (waitMode === 'all') {
        await Promise.all(branchPromises);
      } else if (waitMode === 'any') {
        await Promise.race(branchPromises);
      }
      return { output: { completedBranches: branches.length } };
    }

    case 'for_each': {
      const iteratorField = config.iteratorField as string;
      const items = getNestedValue(ctx, iteratorField) as any[];
      if (!Array.isArray(items)) {
        return { output: { results: [], error: 'iteratorField is not an array' } };
      }

      const concurrency = (config.concurrency as number) || 1;
      const errorPolicy = (config.errorPolicy as string) || 'stop';
      const results: any[] = [];
      const errors: any[] = [];

      // Process items with concurrency control
      for (let i = 0; i < items.length; i += concurrency) {
        const batch = items.slice(i, i + concurrency);
        const batchPromises = batch.map(async (item, batchIdx) => {
          const idx = i + batchIdx;
          // Set iteration context
          ctx['$currentItem'] = item;
          ctx['$index'] = idx;
          try {
            // Execute successor nodes for each item
            const successors = getSuccessorsFromNodes(dsl, [node.id]);
            if (successors.length > 0) {
              await executeGraph(dsl, successors, { ...ctx }, nodeStatuses, instanceId, approvalResults, isCancelled);
            }
            results.push({ index: idx, success: true });
          } catch (err: any) {
            if (errorPolicy === 'stop') throw err;
            errors.push({ index: idx, error: err.message });
          }
        });
        await Promise.all(batchPromises);
      }

      return { output: { results, errors, totalProcessed: items.length } };
    }

    case 'error_handler': {
      // Error handler is special — it wraps the execution of upstream nodes
      // In practice, the error handling is done at the graph execution level
      return { output: { handled: true } };
    }

    // ─── Approval ───────────────────────────────────────────────────
    case 'approval': {
      const timeoutSec = (config.timeout as number) || 86400; // Default 24h
      const timeoutAction = (config.timeoutAction as string) || 'auto_reject';

      // Wait for approval signal
      const gotApproval = await condition(
        () => approvalResults[node.id] !== undefined,
        `${timeoutSec}s`,
      );

      if (!gotApproval) {
        // Timeout
        if (timeoutAction === 'auto_approve') {
          return { output: { approved: true, approvedBy: 'system', comment: 'Auto-approved (timeout)' } };
        } else {
          return { output: { approved: false, approvedBy: 'system', comment: 'Auto-rejected (timeout)' } };
        }
      }

      const approval = approvalResults[node.id];
      return {
        output: { approved: approval.approved, approvedBy: approval.approvedBy, comment: approval.comment },
        branch: approval.approved ? 'true' : 'false',
      };
    }

    // ─── Sub Workflow ────────────────────────────────────────────────
    case 'sub_workflow': {
      const childWorkflowId = config.workflowId as string;
      const inputMapping = config.inputMapping as Record<string, string> || {};
      const childContext: Record<string, any> = {};

      for (const [key, path] of Object.entries(inputMapping)) {
        childContext[key] = getNestedValue(ctx, path);
      }

      // TODO: Load child workflow DSL from DB via activity, then execute as child workflow
      // For now, placeholder
      return { output: { status: 'not_implemented', childWorkflowId } };
    }

    default:
      return assertNever(node.type);
  }
}

// ─── Helper Functions ─────────────────────────────────────────────────────────
// Pure functions are defined in ../dsl/helpers.ts and imported above.

// ─── Cron Trigger Workflow ────────────────────────────────────────────────────
//
// Started by Temporal Schedules (registered via NestJS TemporalService.upsertCronSchedule).
// Creates a t_workflow_instance row with creator='cron', then delegates to dslWorkflow
// as a child workflow so it shares all the per-node logic.

const cronActs = proxyActivities<typeof import('../activities')>({
  startToCloseTimeout: '15s',
  retry: { maximumAttempts: 5 },
});

export interface CronTriggerInput {
  workflowId: number;
  versionId: number;
  dsl: WorkflowDSL;
  scheduleId: string;
}

export async function cronTriggerWorkflow(
  input: CronTriggerInput,
): Promise<WorkflowResult> {
  const { instanceId } = await cronActs.createCronInstance({
    workflowId: input.workflowId,
    versionId: input.versionId,
    scheduleId: input.scheduleId,
  });

  return await executeChild(dslWorkflow, {
    workflowId: `wf-${input.workflowId}-${instanceId}`,
    args: [
      {
        instanceId,
        workflowId: input.workflowId,
        versionId: input.versionId,
        dsl: input.dsl,
        context: { scheduledAt: new Date().toISOString(), scheduleId: input.scheduleId },
      },
    ],
  });
}
