const fs = require('fs');
const path = require('path');
const express = require('express');
const cors = require('cors');
const axios = require('axios');
const {
  startMockApiServer,
  DEFAULT_MOCK_API_PORT,
  buildSampleOperationEnvelope,
} = require('./mock-api');
require('dotenv').config();

const PORT = Number(process.env.PORT || 6522);
const CONDUCTOR_URL = process.env.CONDUCTOR_URL || 'http://127.0.0.1:6520/api';
const CONDUCTOR_ROOT_URL = CONDUCTOR_URL.replace(/\/api\/?$/, '');
const WORKER_ID = process.env.WORKER_ID || 'fresh-hbb-worker-1';
const FLOW_PATH = path.join(__dirname, 'data', 'current-flow.json');
const TEMPLATE_PATH = path.join(__dirname, 'data', 'hbb-ne-template.json');
const WORKFLOW_NAME = 'hbb_ne_call_flow';
const WORKFLOW_VERSION = Number(process.env.WORKFLOW_VERSION || 1);
const MOCK_API_PORT = Number(process.env.MOCK_API_PORT || DEFAULT_MOCK_API_PORT);
const MOCK_API_URL = `http://127.0.0.1:${MOCK_API_PORT}`;
const PRELOAD_TEMPLATE_ON_BOOT = process.env.PRELOAD_TEMPLATE_ON_BOOT !== 'false';

const conductor = axios.create({
  baseURL: CONDUCTOR_URL,
  timeout: 15000,
});

const conductorRoot = axios.create({
  baseURL: CONDUCTOR_ROOT_URL,
  timeout: 15000,
});

const mockApi = axios.create({
  baseURL: MOCK_API_URL,
  timeout: 5000,
});

const app = express();
app.use(cors());
app.use(express.json({ limit: '2mb' }));

const NODE_TO_MOCK_OPERATION = {
  'fraud-check': 'fraud-check',
  'rmn-nrmn': 'rmn-nrmn',
  'application-transfer-check': 'application-transfer-check',
  'language-selection': 'language-selection',
  'hbb-callers': 'hbb-callers',
  'enter-hbb-number': 'enter-hbb-number',
  'select-hbb-number': 'select-hbb-number',
  'buy-bundles': 'buy-bundles',
  'airtel-money-services': 'airtel-money-services',
  'check-balance': 'check-balance',
  'open-ticket': 'open-ticket',
  'billing-inquiry': 'billing-inquiry',
  'connectivity-settings': 'connectivity-settings',
  'general-information': 'general-information',
  'business-hours-check': 'business-hours-check',
  'sms-gateway': 'sms-gateway',
};

function loadJson(filePath) {
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}

function saveJson(filePath, data) {
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
}

function loadFlow() {
  const template = loadJson(TEMPLATE_PATH);

  if (PRELOAD_TEMPLATE_ON_BOOT) {
    saveJson(FLOW_PATH, template);
    return template;
  }

  if (!fs.existsSync(FLOW_PATH)) {
    saveJson(FLOW_PATH, template);
    return template;
  }

  return loadJson(FLOW_PATH);
}

let currentFlow = loadFlow();

function sanitizeRefName(id) {
  return String(id)
    .toLowerCase()
    .replace(/[^a-z0-9_]/g, '_')
    .replace(/_+/g, '_')
    .slice(0, 55);
}

function linearizeFlow(flow) {
  const nodes = flow.nodes || [];
  const edges = flow.edges || [];
  const byId = new Map(nodes.map((node) => [node.id, node]));
  const outgoing = new Map();
  const incomingCount = new Map();

  for (const node of nodes) {
    outgoing.set(node.id, []);
    incomingCount.set(node.id, 0);
  }

  for (const edge of edges) {
    if (!byId.has(edge.source) || !byId.has(edge.target)) continue;
    outgoing.get(edge.source).push(edge.target);
    incomingCount.set(edge.target, (incomingCount.get(edge.target) || 0) + 1);
  }

  const starts = nodes
    .filter((node) => node.nodeType === 'start' || (incomingCount.get(node.id) || 0) === 0)
    .sort((a, b) => {
      const pa = flow.positions?.[a.id] || { y: 0, x: 0 };
      const pb = flow.positions?.[b.id] || { y: 0, x: 0 };
      return pa.y - pb.y || pa.x - pb.x;
    });

  const queue = [...starts];
  const visited = new Set();
  const ordered = [];

  while (queue.length > 0) {
    const node = queue.shift();
    if (!node || visited.has(node.id)) continue;
    visited.add(node.id);
    ordered.push(node);

    for (const targetId of outgoing.get(node.id) || []) {
      if (!visited.has(targetId)) {
        queue.push(byId.get(targetId));
      }
    }
  }

  for (const node of nodes) {
    if (!visited.has(node.id)) ordered.push(node);
  }

  return ordered.filter((node) => node.nodeType !== 'start' && node.nodeType !== 'end');
}

async function ensureTaskDefinition() {
  const taskDef = {
    name: 'hbb_step_task',
    description: 'Generic HBB IVR flow execution step',
    ownerEmail: 'flowbuilder@local',
    retryCount: 1,
    timeoutSeconds: 60,
    responseTimeoutSeconds: 30,
  };

  try {
    await conductor.post('/metadata/taskdefs', [taskDef]);
  } catch (error) {
    if (error.response?.status !== 409) {
      throw error;
    }
  }
}

function buildWorkflowDefinition(flow) {
  const orderedNodes = linearizeFlow(flow);

  return {
    name: WORKFLOW_NAME,
    description: 'Fresh HBB flow built from AA_NE_HBB_Call flow.pdf',
    version: 1,
    ownerEmail: 'flowbuilder@local',
    schemaVersion: 2,
    restartable: true,
    workflowStatusListenerEnabled: false,
    timeoutPolicy: 'ALERT_ONLY',
    timeoutSeconds: 0,
    inputParameters: ['sessionId', 'callerNumber'],
    tasks: orderedNodes.map((node, index) => ({
      name: 'hbb_step_task',
      taskReferenceName: `${sanitizeRefName(node.id)}_${index + 1}`,
      type: 'SIMPLE',
      startDelay: 0,
      optional: false,
      inputParameters: {
        nodeId: node.id,
        nodeLabel: node.label,
        nodeType: node.nodeType,
        pageRef: node.pageRef || null,
        description: node.description || null,
      },
    })),
  };
}

async function upsertWorkflowDefinition(workflowDef) {
  try {
    await conductor.post('/metadata/workflow', workflowDef);
  } catch (error) {
    if (error.response?.status === 409) {
      await conductor.put('/metadata/workflow', [workflowDef]);
      return;
    }
    throw error;
  }
}

async function deployFlowToConductor(flow) {
  await ensureTaskDefinition();
  const workflowDef = buildWorkflowDefinition(flow);
  await upsertWorkflowDefinition(workflowDef);
  return workflowDef;
}

function getMockOperation(nodeId) {
  if (!nodeId) return null;
  return NODE_TO_MOCK_OPERATION[nodeId] || null;
}

function validateFlowPayload(flow) {
  return flow && Array.isArray(flow.nodes) && Array.isArray(flow.edges);
}

function buildApiToolCalls(flow) {
  const nodesById = new Map((flow.nodes || []).map((node) => [node.id, node]));
  const callerNumber = '22700000000';

  return Object.entries(NODE_TO_MOCK_OPERATION)
    .filter(([nodeId]) => nodesById.has(nodeId))
    .map(([nodeId, operation]) => {
      const node = nodesById.get(nodeId);
      const sampleRequest = {
        workflowId: '<workflow-instance-id>',
        taskId: '<task-id>',
        nodeId,
        nodeLabel: node?.label || null,
        callerNumber,
      };

      return {
        nodeId,
        nodeLabel: node?.label || null,
        operation,
        method: 'POST',
        endpoint: `${MOCK_API_URL}/internal/${operation}`,
        requestBody: sampleRequest,
        sampleResponse: buildSampleOperationEnvelope(operation, sampleRequest),
      };
    });
}

function buildExportPayload(flow) {
  return {
    exportedAt: new Date().toISOString(),
    workflow: {
      name: WORKFLOW_NAME,
      version: WORKFLOW_VERSION,
    },
    flow,
    mockApi: {
      baseUrl: MOCK_API_URL,
      nodeMappings: NODE_TO_MOCK_OPERATION,
      apiToolCalls: buildApiToolCalls(flow),
    },
  };
}

async function callInternalMockApi(task) {
  const nodeId = task.inputData?.nodeId;
  const operation = getMockOperation(nodeId);
  if (!operation) return null;

  const payload = {
    workflowId: task.workflowInstanceId,
    taskId: task.taskId,
    nodeId,
    nodeLabel: task.inputData?.nodeLabel || null,
    callerNumber: task.inputData?.callerNumber || '22700000000',
  };

  const response = await mockApi.post(`/internal/${operation}`, payload);
  return response.data;
}

let isPolling = false;

async function pollAndCompleteTask() {
  if (isPolling) return;
  isPolling = true;

  try {
    const response = await conductor.get('/tasks/poll/hbb_step_task', {
      params: { workerid: WORKER_ID },
    });

    const task = response.data;
    if (!task || !task.taskId) return;

    let mockApiResult = null;
    let mockApiError = null;
    try {
      mockApiResult = await callInternalMockApi(task);
    } catch (error) {
      mockApiError = error.message;
    }

    await conductor.post('/tasks', {
      taskId: task.taskId,
      workflowInstanceId: task.workflowInstanceId,
      workerId: WORKER_ID,
      status: 'COMPLETED',
      outputData: {
        completedAt: new Date().toISOString(),
        message: `Step completed: ${task.inputData?.nodeLabel || task.inputData?.nodeId || 'unknown'}`,
        nodeId: task.inputData?.nodeId || null,
        nodeType: task.inputData?.nodeType || null,
        mockApiResult,
        mockApiError,
      },
    });
  } catch (error) {
    const status = error.response?.status;
    if (status !== 404 && status !== 204 && status !== 408) {
      console.error('[Worker] Poll error:', error.message);
    }
  } finally {
    isPolling = false;
  }
}

setInterval(pollAndCompleteTask, 1000);

app.get('/api/health', (_req, res) => {
  res.json({
    success: true,
    service: 'fresh-hbb-flowbuilder-backend',
    conductorUrl: CONDUCTOR_URL,
    mockApiUrl: MOCK_API_URL,
  });
});

app.get('/api/flow', (_req, res) => {
  res.json({ success: true, flow: currentFlow });
});

app.put('/api/flow', (req, res) => {
  const { flow } = req.body;
  if (!validateFlowPayload(flow)) {
    return res.status(400).json({ success: false, error: 'Invalid flow payload' });
  }

  currentFlow = flow;
  saveJson(FLOW_PATH, currentFlow);
  return res.json({ success: true, flow: currentFlow });
});

app.post('/api/flow/reset-template', (_req, res) => {
  currentFlow = loadJson(TEMPLATE_PATH);
  saveJson(FLOW_PATH, currentFlow);
  res.json({ success: true, flow: currentFlow });
});

app.post('/api/flow/export', (req, res) => {
  const flow = req.body?.flow || currentFlow;
  if (!validateFlowPayload(flow)) {
    return res.status(400).json({ success: false, error: 'Invalid flow payload' });
  }

  const payload = buildExportPayload(flow);
  return res.json({ success: true, export: payload });
});

app.get('/api/conductor/health', async (_req, res) => {
  try {
    const response = await conductorRoot.get('/health');
    res.json({ success: true, data: response.data });
  } catch (error) {
    res.status(502).json({
      success: false,
      error: 'Conductor is unreachable',
      details: error.message,
      conductorUrl: CONDUCTOR_ROOT_URL,
    });
  }
});

app.get('/api/mock-api/health', async (_req, res) => {
  try {
    const response = await mockApi.get('/health');
    res.json({ success: true, data: response.data });
  } catch (error) {
    res.status(502).json({
      success: false,
      error: 'Internal mock API is unreachable',
      details: error.message,
      mockApiUrl: MOCK_API_URL,
    });
  }
});

app.get('/api/mock-api/mappings', (_req, res) => {
  res.json({
    success: true,
    mockApiUrl: MOCK_API_URL,
    mappings: NODE_TO_MOCK_OPERATION,
  });
});

app.post('/api/conductor/deploy', async (req, res) => {
  try {
    const flow = req.body?.flow || currentFlow;
    const workflowDef = await deployFlowToConductor(flow);

    res.json({
      success: true,
      workflowName: workflowDef.name,
      version: workflowDef.version,
      steps: workflowDef.tasks.length,
    });
  } catch (error) {
    console.error('[Deploy] Error:', error.message);
    res.status(500).json({ success: false, error: error.message });
  }
});

app.post('/api/conductor/start', async (req, res) => {
  try {
    const workflowName = WORKFLOW_NAME;
    const input = {
      sessionId: req.body?.sessionId || `session-${Date.now()}`,
      callerNumber: req.body?.callerNumber || '0000000000',
      startedFromUi: true,
    };

    const response = await conductor.post(
      `/workflow/${workflowName}`,
      input,
      { params: { version: WORKFLOW_VERSION } },
    );
    res.json({
      success: true,
      workflowId: response.data,
      workflowName,
      version: WORKFLOW_VERSION,
      input,
    });
  } catch (error) {
    console.error('[Start] Error:', error.message);
    res.status(500).json({ success: false, error: error.response?.data || error.message });
  }
});

app.get('/api/conductor/status/:workflowId', async (req, res) => {
  try {
    const workflowId = req.params.workflowId;
    const response = await conductor.get(`/workflow/${workflowId}`, {
      params: { includeTasks: true },
    });

    const workflow = response.data;
    const tasks = (workflow.tasks || []).map((task) => ({
      taskId: task.taskId,
      refName: task.referenceTaskName,
      taskType: task.taskType,
      status: task.status,
      nodeId: task.inputData?.nodeId || null,
      nodeLabel: task.inputData?.nodeLabel || null,
      output: task.outputData || null,
    }));

    res.json({
      success: true,
      workflowId,
      status: workflow.status,
      startTime: workflow.startTime,
      endTime: workflow.endTime,
      tasks,
    });
  } catch (error) {
    res.status(500).json({ success: false, error: error.response?.data || error.message });
  }
});

app.listen(PORT, () => {
  console.log(`Fresh HBB Flow Builder backend listening on http://localhost:${PORT}`);
  console.log(`Conductor target: ${CONDUCTOR_URL}`);
  console.log(`Internal mock API target: ${MOCK_API_URL}`);
});

startMockApiServer({ port: MOCK_API_PORT, host: '127.0.0.1' });
