import { parseBpmnXml } from './bpmn-parser'
import type {
  SimulationInput,
  SimulationResult,
  ActivitySimResult,
  ResourceSimResult,
  ProcessGraph,
  BpmnElementId,
} from './types'
import { isGatewayNode } from './types'

// ─── Detección de back-edges (loops) via DFS con coloreado ──────────────────

function findBackEdges(graph: ProcessGraph): Set<string> {
  const backEdges = new Set<string>()
  const WHITE = 0, GRAY = 1, BLACK = 2
  const color = new Map<BpmnElementId, number>()

  function dfs(nodeId: BpmnElementId) {
    color.set(nodeId, GRAY)
    const node = graph.nodes.get(nodeId)
    if (!node) { color.set(nodeId, BLACK); return }

    for (const flowId of node.outgoing) {
      const flow = graph.flows.get(flowId)
      if (!flow) continue
      const targetColor = color.get(flow.targetRef) ?? WHITE
      if (targetColor === GRAY) {
        backEdges.add(flowId)
      } else if (targetColor === WHITE) {
        dfs(flow.targetRef)
      }
    }
    color.set(nodeId, BLACK)
  }

  for (const startId of graph.startEventIds) {
    if ((color.get(startId) ?? 0) === 0) dfs(startId)
  }
  return backEdges
}

// ─── Detección de loops con mensajes accionables ──────────────────────────────

function detectLoops(graph: ProcessGraph, backEdges: Set<string>): string[] {
  const warnings: string[] = []
  const seen = new Set<string>()

  for (const flowId of backEdges) {
    const flow = graph.flows.get(flowId)
    if (!flow || seen.has(flow.targetRef)) continue
    seen.add(flow.targetRef)
    const name = graph.nodes.get(flow.targetRef)?.name || flow.targetRef
    warnings.push(
      `Loop detectado en el proceso: el nodo "${name}" forma un ciclo. Los costos se calcularán asumiendo una sola ejecución del ciclo.`
    )
  }
  return warnings
}

// ─── Sort topológico (Kahn's algorithm, ignora back-edges) ───────────────────

function topologicalSort(graph: ProcessGraph, backEdges: Set<string>): BpmnElementId[] {
  const inDegree = new Map<BpmnElementId, number>()
  for (const nodeId of graph.nodes.keys()) inDegree.set(nodeId, 0)

  for (const [flowId, flow] of graph.flows) {
    if (backEdges.has(flowId)) continue
    inDegree.set(flow.targetRef, (inDegree.get(flow.targetRef) ?? 0) + 1)
  }

  const queue: BpmnElementId[] = []
  for (const [nodeId, deg] of inDegree) {
    if (deg === 0) queue.push(nodeId)
  }

  const order: BpmnElementId[] = []
  while (queue.length > 0) {
    const nodeId = queue.shift()!
    order.push(nodeId)
    const node = graph.nodes.get(nodeId)
    if (!node) continue
    for (const flowId of node.outgoing) {
      if (backEdges.has(flowId)) continue
      const flow = graph.flows.get(flowId)
      if (!flow) continue
      const newDeg = (inDegree.get(flow.targetRef) ?? 0) - 1
      inDegree.set(flow.targetRef, newDeg)
      if (newDeg === 0) queue.push(flow.targetRef)
    }
  }
  return order
}

// ─── Propagación de probabilidades en orden topológico ───────────────────────
//
// Cada nodo se visita exactamente una vez, con su probabilidad ya acumulada.
// Esto evita el overcounting del BFS (donde nodos convergentes se visitaban
// múltiples veces con probabilidades parciales).
//
// Reglas de acumulación en el nodo TARGET:
//   AND-join (parallelGateway convergente): max(existing, branchProb)
//     — las ramas paralelas son instancias del mismo flujo, no alternativas.
//   Todo lo demás (XOR-join, secuencia, eventos): sum clampeado a 1.0
//     — representan caminos mutuamente excluyentes.

function computeExecutionProbabilities(
  graph: ProcessGraph,
  gatewayProbs: Map<BpmnElementId, Map<string, number>>,
  backEdges: Set<string>
): Map<BpmnElementId, number> {

  const andJoinIds = new Set<BpmnElementId>()
  for (const node of graph.nodes.values()) {
    if (node.type === 'parallelGateway' && node.incoming.length > 1 && node.outgoing.length <= 1) {
      andJoinIds.add(node.id)
    }
  }

  const probs = new Map<BpmnElementId, number>()
  for (const startId of graph.startEventIds) probs.set(startId, 1.0)

  const topoOrder = topologicalSort(graph, backEdges)

  for (const nodeId of topoOrder) {
    const node = graph.nodes.get(nodeId)
    if (!node) continue
    const nodeProb = probs.get(nodeId) ?? 0

    for (const flowId of node.outgoing) {
      if (backEdges.has(flowId)) continue
      const flow = graph.flows.get(flowId)
      if (!flow) continue

      let branchProb = nodeProb

      if (isGatewayNode(node.type)) {
        const gwFlowProbs = gatewayProbs.get(nodeId)
        if (gwFlowProbs) {
          branchProb = nodeProb * (gwFlowProbs.get(flowId) ?? 0)
        } else {
          branchProb = node.type === 'parallelGateway'
            ? nodeProb
            : nodeProb / Math.max(node.outgoing.length, 1)
        }
      }

      const targetId = flow.targetRef
      const existing = probs.get(targetId) ?? 0
      const accumulated = andJoinIds.has(targetId)
        ? Math.max(existing, branchProb)
        : Math.min(existing + branchProb, 1.0)

      probs.set(targetId, accumulated)
    }
  }

  return probs
}

// ─── Motor de simulación principal ───────────────────────────────────────────

export function runSimulation(input: SimulationInput): SimulationResult {
  const { processXml, activities, gateways, resources, globalSettings } = input
  const warnings: string[] = []

  const graph = parseBpmnXml(processXml)

  const backEdges = findBackEdges(graph)
  warnings.push(...detectLoops(graph, backEdges))

  const gatewayFlowProbs = new Map<BpmnElementId, Map<string, number>>()
  for (const [elemId, gwConfig] of gateways.entries()) {
    const flowMap = new Map<string, number>()
    for (const branch of gwConfig.branches) flowMap.set(branch.flowId, branch.probability)
    gatewayFlowProbs.set(elemId, flowMap)
  }

  const execProbs = computeExecutionProbabilities(graph, gatewayFlowProbs, backEdges)

  const perActivity: ActivitySimResult[] = []
  const resourceTotals = new Map<string, { cost: number; minutes: number; name: string }>()

  // Seleccionar campos del escenario: para actividades automatable, usar costos automatizados
  const scenario = input.scenario ?? 'actual'

  for (const activity of activities.values()) {
    const execProb = execProbs.get(activity.bpmnElementId) ?? 0
    const node = graph.nodes.get(activity.bpmnElementId)

    if (!node) {
      warnings.push(
        `La actividad "${activity.name}" (${activity.bpmnElementId}) no se encontró en el diagrama y se omitirá del cálculo.`
      )
      continue
    }

    // En escenario automatizado, usar campos automatizados solo para actividades marcadas;
    // el resto mantiene sus costos originales (regla de negocio central del Sprint 1).
    const useAutomated = scenario === 'automated' && activity.automatable
    const directCostFixed = useAutomated ? activity.automatedCostFixed : activity.directCostFixed
    const executionTimeMinutes = useAutomated ? activity.automatedTimeMinutes : activity.executionTimeMinutes

    const resourceBreakdown: ActivitySimResult['resourceCostBreakdown'] = []
    let resourceCostDirect = 0

    const resourceAssignments = useAutomated
      ? (activity.automatedAssignedResources ?? [])
      : activity.assignedResources

    for (const assignment of resourceAssignments) {
      const resource = resources.get(assignment.resourceId)
      if (!resource) continue
      const hoursUsed = (assignment.utilizationMinutes / 60) * assignment.units
      const cost = resource.costPerHour * hoursUsed * execProb
      resourceBreakdown.push({ resourceId: resource.id, resourceName: resource.name, cost })
      resourceCostDirect += cost

      const prev = resourceTotals.get(resource.id) ?? { cost: 0, minutes: 0, name: resource.name }
      resourceTotals.set(resource.id, {
        cost: prev.cost + cost,
        minutes: prev.minutes + assignment.utilizationMinutes * assignment.units * execProb,
        name: resource.name,
      })
    }

    const fixedCostExpected = directCostFixed * execProb
    const totalExpectedDirectCost = fixedCostExpected + resourceCostDirect

    perActivity.push({
      activityId: activity.id,
      bpmnElementId: activity.bpmnElementId,
      activityName: activity.name || activity.bpmnElementId,
      expectedDirectCost: totalExpectedDirectCost,
      expectedIndirectCost: 0,
      expectedTotalCost: 0,
      percentOfTotal: 0,
      expectedExecutions: execProb,
      executionProbability: execProb,
      resourceCostBreakdown: resourceBreakdown,
      executionTimeMinutes,
    })
  }

  const totalDirectCost = perActivity.reduce((s, a) => s + a.expectedDirectCost, 0)
  const totalIndirectCost = totalDirectCost * globalSettings.overheadPercentage
  const totalCost = totalDirectCost + totalIndirectCost

  for (const act of perActivity) {
    const directRatio = totalDirectCost > 0 ? act.expectedDirectCost / totalDirectCost : 0
    act.expectedIndirectCost = totalIndirectCost * directRatio
    act.expectedTotalCost = act.expectedDirectCost + act.expectedIndirectCost
    act.percentOfTotal = totalCost > 0 ? (act.expectedTotalCost / totalCost) * 100 : 0
  }

  perActivity.sort((a, b) => b.expectedTotalCost - a.expectedTotalCost)

  const perResource: ResourceSimResult[] = Array.from(resourceTotals.entries()).map(([id, data]) => ({
    resourceId: id, resourceName: data.name, totalCost: data.cost, totalMinutesUsed: data.minutes,
  }))

  const totalTimeMinutes = perActivity.reduce(
    (s, a) => s + a.executionTimeMinutes * a.executionProbability,
    0
  )

  return { totalCost, totalDirectCost, totalIndirectCost, totalTimeMinutes, perActivity, perResource, warnings }
}
