import { AUTOMATION_VARIABLES } from './catalog';
import type {
  AutomationNodeType,
  FlowEdge,
  FlowNode,
  ValidationIssue,
  ValidationResult,
} from './types';

const NODE_TYPES = new Set<AutomationNodeType>([
  'TRIGGER',
  'CONDITION',
  'ACTION',
  'WAIT',
  'BRANCH',
  'END',
]);

const MESSAGE_CHANNELS = new Set(['EMAIL', 'WHATSAPP', 'PUSH']);
const KNOWN_VARIABLES = new Set<string>(
  AUTOMATION_VARIABLES.map((item) => item.token),
);

function hasValue(value: unknown): boolean {
  return value !== undefined && value !== null && value !== '';
}

function issue(input: ValidationIssue): ValidationIssue {
  return input;
}

function hasMessageContent(config: Record<string, unknown>): boolean {
  return hasValue(config.templateId) || hasValue(config.body);
}

function isMessageAction(node: FlowNode): boolean {
  const channel = node.config.channel;
  return (
    (typeof channel === 'string' && MESSAGE_CHANNELS.has(channel)) ||
    node.label.toLowerCase().startsWith('enviar')
  );
}

function isWebhookAction(node: FlowNode): boolean {
  const channel = node.config.channel;
  return (
    channel === 'WEBHOOK' ||
    node.label.toLowerCase().includes('webhook') ||
    hasValue(node.config.url)
  );
}

function hasValidHttpUrl(value: unknown): boolean {
  if (typeof value !== 'string') return false;
  try {
    const url = new URL(value);
    return url.protocol === 'http:' || url.protocol === 'https:';
  } catch {
    return false;
  }
}

function numericTotal(items: unknown[]): number {
  return items.reduce<number>((sum, item) => {
    if (item === null || typeof item !== 'object') return sum;
    const record = item as Record<string, unknown>;
    const rawValue =
      record.percentage ?? record.percent ?? record.weight ?? record.value;
    const value = typeof rawValue === 'number' ? rawValue : Number(rawValue);
    return Number.isFinite(value) ? sum + value : sum;
  }, 0);
}

function collectStrings(value: unknown, output: string[] = []): string[] {
  if (typeof value === 'string') {
    output.push(value);
    return output;
  }
  if (Array.isArray(value)) {
    for (const item of value) collectStrings(item, output);
    return output;
  }
  if (value !== null && typeof value === 'object') {
    for (const item of Object.values(value)) collectStrings(item, output);
  }
  return output;
}

function findUnknownVariables(config: Record<string, unknown>): string[] {
  const found = new Set<string>();
  for (const value of collectStrings(config)) {
    for (const match of value.match(/{{\s*[^}]+\s*}}/g) ?? []) {
      const normalized = match.replace(/\s+/g, '');
      if (!KNOWN_VARIABLES.has(normalized)) found.add(normalized);
    }
  }
  return [...found];
}

function detectCycles(nodes: FlowNode[], edges: FlowEdge[]): ValidationIssue[] {
  const byKey = new Map(nodes.map((node) => [node.nodeKey, node]));
  const outgoing = new Map<string, string[]>();
  for (const edge of edges) {
    const targets = outgoing.get(edge.sourceNodeKey) ?? [];
    targets.push(edge.targetNodeKey);
    outgoing.set(edge.sourceNodeKey, targets);
  }

  const issues: ValidationIssue[] = [];
  const emitted = new Set<string>();
  const visiting = new Set<string>();
  const visited = new Set<string>();
  const stack: string[] = [];

  function visit(nodeKey: string) {
    if (visiting.has(nodeKey)) {
      const start = stack.indexOf(nodeKey);
      const cycle = start >= 0 ? stack.slice(start) : [nodeKey];
      const hasBreaker = cycle.some((key) => {
        const type = byKey.get(key)?.type;
        return type === 'WAIT' || type === 'END';
      });
      if (!hasBreaker && !emitted.has(nodeKey)) {
        const node = byKey.get(nodeKey);
        emitted.add(nodeKey);
        issues.push(
          issue({
            nodeKey,
            code: 'INFINITE_LOOP',
            message: `Possível laço infinito a partir de '${node?.label ?? nodeKey}'.`,
            severity: 'error',
          }),
        );
      }
      return;
    }

    if (visited.has(nodeKey)) return;
    visiting.add(nodeKey);
    stack.push(nodeKey);
    for (const target of outgoing.get(nodeKey) ?? []) {
      if (byKey.has(target)) visit(target);
    }
    stack.pop();
    visiting.delete(nodeKey);
    visited.add(nodeKey);
  }

  for (const node of nodes) visit(node.nodeKey);
  return issues;
}

export function validateFlow(
  nodes: FlowNode[],
  edges: FlowEdge[],
): ValidationResult {
  const issues: ValidationIssue[] = [];
  const triggers = nodes.filter((node) => node.type === 'TRIGGER');
  const nodeByKey = new Map(
    nodes.filter((node) => node.nodeKey).map((node) => [node.nodeKey, node]),
  );
  const incomingCount = new Map<string, number>();
  const outgoingCount = new Map<string, number>();

  for (const edge of edges) {
    incomingCount.set(
      edge.targetNodeKey,
      (incomingCount.get(edge.targetNodeKey) ?? 0) + 1,
    );
    outgoingCount.set(
      edge.sourceNodeKey,
      (outgoingCount.get(edge.sourceNodeKey) ?? 0) + 1,
    );
  }

  if (triggers.length === 0) {
    issues.push(
      issue({
        code: 'NO_TRIGGER',
        message: 'O fluxo precisa de exatamente um gatilho inicial.',
        severity: 'error',
      }),
    );
  } else if (triggers.length > 1) {
    issues.push(
      issue({
        code: 'MULTIPLE_TRIGGERS',
        message: 'O fluxo só pode ter um gatilho inicial.',
        severity: 'error',
      }),
    );
  }

  for (const node of nodes) {
    if (!hasValue(node.nodeKey)) {
      issues.push(
        issue({
          code: 'NODE_MISSING_KEY',
          message: 'Há um bloco sem identificador.',
          severity: 'error',
        }),
      );
      continue;
    }

    if (!NODE_TYPES.has(node.type)) {
      issues.push(
        issue({
          nodeKey: node.nodeKey,
          code: 'INVALID_NODE_TYPE',
          message: `Tipo de bloco inválido: ${node.type}.`,
          severity: 'error',
        }),
      );
    }

    if (node.type !== 'TRIGGER' && (incomingCount.get(node.nodeKey) ?? 0) < 1) {
      issues.push(
        issue({
          nodeKey: node.nodeKey,
          code: 'ORPHAN_NODE',
          message: `O bloco '${node.label}' está desconectado.`,
          severity: 'error',
        }),
      );
    }

    if (node.type !== 'END' && (outgoingCount.get(node.nodeKey) ?? 0) < 1) {
      issues.push(
        issue({
          nodeKey: node.nodeKey,
          code: 'ORPHAN_NODE',
          message: `O bloco '${node.label}' está desconectado.`,
          severity: 'error',
        }),
      );
    }

    if (node.type === 'ACTION') {
      if (Object.keys(node.config ?? {}).length === 0) {
        issues.push(
          issue({
            nodeKey: node.nodeKey,
            code: 'ACTION_MISSING_CONFIG',
            message: `A ação '${node.label}' está sem configuração obrigatória.`,
            severity: 'error',
          }),
        );
      }

      if (isMessageAction(node)) {
        if (!hasValue(node.config.channel)) {
          issues.push(
            issue({
              nodeKey: node.nodeKey,
              code: 'MESSAGE_MISSING_CHANNEL',
              message: `A mensagem '${node.label}' precisa de um canal.`,
              severity: 'error',
            }),
          );
        }
        if (!hasMessageContent(node.config)) {
          issues.push(
            issue({
              nodeKey: node.nodeKey,
              code: 'MESSAGE_MISSING_CONTENT',
              message: `A mensagem '${node.label}' precisa de template ou conteúdo.`,
              severity: 'error',
            }),
          );
        }
      }

      if (node.label === 'Criar tarefa' && !hasValue(node.config.title)) {
        issues.push(
          issue({
            nodeKey: node.nodeKey,
            code: 'ACTION_MISSING_CONFIG',
            message: `A ação '${node.label}' está sem configuração obrigatória.`,
            severity: 'error',
          }),
        );
      }

      if (isWebhookAction(node) && !hasValidHttpUrl(node.config.url)) {
        issues.push(
          issue({
            nodeKey: node.nodeKey,
            code: 'WEBHOOK_INVALID_URL',
            message: `O webhook '${node.label}' precisa de uma URL válida.`,
            severity: 'error',
          }),
        );
      }
    }

    if (
      node.type === 'WAIT' &&
      (!hasValue(node.config.unit) ||
        typeof node.config.duration !== 'number' ||
        node.config.duration <= 0)
    ) {
      issues.push(
        issue({
          nodeKey: node.nodeKey,
          code: 'WAIT_MISSING_DURATION',
          message: `A espera '${node.label}' precisa de duração e unidade.`,
          severity: 'error',
        }),
      );
    }

    if (node.type === 'CONDITION') {
      if (!hasValue(node.config.operator) || !hasValue(node.config.value)) {
        issues.push(
          issue({
            nodeKey: node.nodeKey,
            code: 'CONDITION_MISSING_OPERATOR',
            message: `A condição '${node.label}' precisa de operador e valor.`,
            severity: 'error',
          }),
        );
      }

      const hasFalsePath = edges.some(
        (edge) =>
          edge.sourceNodeKey === node.nodeKey && edge.condition === 'false',
      );
      if (!hasFalsePath) {
        issues.push(
          issue({
            nodeKey: node.nodeKey,
            code: 'CONDITION_MISSING_FALSE_PATH',
            message: `A condição '${node.label}' precisa de um caminho 'não'.`,
            severity: 'error',
          }),
        );
      }
    }

    if (node.type === 'BRANCH') {
      const distribution = node.config.distribution;
      if (Array.isArray(distribution) && numericTotal(distribution) !== 100) {
        issues.push(
          issue({
            nodeKey: node.nodeKey,
            code: 'SPLIT_NOT_100',
            message: `A ramificação '${node.label}' precisa somar 100%.`,
            severity: 'error',
          }),
        );
      }
    }

    for (const variable of findUnknownVariables(node.config)) {
      issues.push(
        issue({
          nodeKey: node.nodeKey,
          code: 'UNRESOLVED_VARIABLE',
          message: `Variável desconhecida em '${node.label}': ${variable}.`,
          severity: 'warning',
        }),
      );
    }
  }

  for (const edge of edges) {
    if (
      !nodeByKey.has(edge.sourceNodeKey) ||
      !nodeByKey.has(edge.targetNodeKey)
    ) {
      const node =
        nodeByKey.get(edge.sourceNodeKey) ?? nodeByKey.get(edge.targetNodeKey);
      issues.push(
        issue({
          nodeKey: node?.nodeKey,
          code: 'ORPHAN_NODE',
          message: `O bloco '${node?.label ?? edge.edgeKey}' está desconectado.`,
          severity: 'error',
        }),
      );
    }
  }

  issues.push(...detectCycles(nodes, edges));

  return {
    valid: issues.every((item) => item.severity !== 'error'),
    issues,
  };
}
