/**
 * Ticket Workflow Engine
 * Refactored table-driven workflow engine with per-workflow and per-step toggles.
 * Matches ticket events to workflows, executes steps sequentially, resolves template
 * variables, and accumulates context between steps.
 */

import { postgresClient } from './postgres-client';
import {
  TicketWorkflow,
  TicketWorkflowStep,
  TicketWorkflowWithSteps,
  TicketWorkflowExecution,
  WorkflowStepContext,
  WorkflowStepExecutorFn,
  WorkflowStepResult,
  TriggerCondition,
  StepCondition,
} from '../types/ticket-workflow';
import { TicketData, WorkflowSettings } from '../types/workflow';

// Step executor registry — populated by individual step files
const workflowStepExecutors: Map<string, WorkflowStepExecutorFn> = new Map();

export function registerWorkflowStepExecutor(stepType: string, executor: WorkflowStepExecutorFn): void {
  workflowStepExecutors.set(stepType, executor);
}

export class TicketWorkflowEngine {
  /**
   * Main entry point: process a ticket event and execute matching workflows.
   */
  async processTrigger(
    triggerEvent: string,
    ticket: TicketData
  ): Promise<number[]> {
    // Check global kill switch
    const settings = await this.getSettings();
    if (!settings.workflow_engine_enabled) {
      console.log('[TICKET-WORKFLOW] Engine is disabled globally, skipping');
      return [];
    }

    // Find matching workflows
    const workflows = await this.findMatchingWorkflows(triggerEvent, ticket);

    if (workflows.length === 0) {
      console.log(`[TICKET-WORKFLOW] No workflows matched for ${triggerEvent} on ticket #${ticket.ticket_number}`);
      return [];
    }

    console.log(`[TICKET-WORKFLOW] ${workflows.length} workflow(s) matched for ticket #${ticket.ticket_number}`);

    // Execute each matching workflow
    const executionIds: number[] = [];
    for (const workflow of workflows) {
      try {
        const execId = await this.executeWorkflow(workflow, ticket, settings);
        executionIds.push(execId);
      } catch (err) {
        console.error(`[TICKET-WORKFLOW] Failed to execute workflow "${workflow.name}":`, err);
      }
    }

    return executionIds;
  }

  /**
   * Load workflow settings from DB.
   */
  async getSettings(): Promise<WorkflowSettings> {
    const result = await postgresClient.query(
      `SELECT key, value FROM workflow_settings`
    );

    const raw: Record<string, any> = {};
    for (const row of result.rows) {
      try {
        raw[row.key] = JSON.parse(row.value);
      } catch {
        raw[row.key] = row.value;
      }
    }

    return {
      workflow_engine_enabled: raw.workflow_engine_enabled ?? false,
      default_ai_provider: raw.default_ai_provider ?? 'openai',
      openai_api_key: raw.openai_api_key ?? '',
      openai_model: raw.openai_model ?? 'gpt-4o',
      anthropic_api_key: raw.anthropic_api_key ?? '',
      anthropic_model: raw.anthropic_model ?? 'claude-sonnet-4-20250514',
      ai_for_title_cleanup: raw.ai_for_title_cleanup ?? true,
      ai_for_description_rewrite: raw.ai_for_description_rewrite ?? true,
      ai_for_ambiguous_classification: raw.ai_for_ambiguous_classification ?? true,
      ai_for_troubleshooting: raw.ai_for_troubleshooting ?? true,
      autotask_update_delay_ms: Number(raw.autotask_update_delay_ms) || 30000,
      max_ai_retries: Number(raw.max_ai_retries) || 2,
      classification_confidence_threshold: raw.classification_confidence_threshold ?? 'medium',
      log_retention_days: Number(raw.log_retention_days) || 90,
    };
  }

  /**
   * Find active workflows matching the trigger event and conditions.
   */
  async findMatchingWorkflows(
    triggerEvent: string,
    ticket: TicketData
  ): Promise<TicketWorkflowWithSteps[]> {
    const result = await postgresClient.query<TicketWorkflow>(
      `SELECT * FROM ticket_workflows
       WHERE is_active = true AND trigger_event = $1
       ORDER BY sort_order`,
      [triggerEvent]
    );

    const matched: TicketWorkflowWithSteps[] = [];

    for (const workflow of result.rows) {
      const conditions: TriggerCondition[] = Array.isArray(workflow.trigger_conditions)
        ? workflow.trigger_conditions
        : [];

      if (this.evaluateTriggerConditions(conditions, ticket)) {
        const stepsResult = await postgresClient.query<TicketWorkflowStep>(
          `SELECT * FROM ticket_workflow_steps
           WHERE workflow_id = $1 AND is_active = true
           ORDER BY step_order`,
          [workflow.id]
        );
        matched.push({ ...workflow, steps: stepsResult.rows });
      }
    }

    return matched;
  }

  /**
   * Execute a single workflow: create execution record, run steps, update status.
   */
  async executeWorkflow(
    workflow: TicketWorkflowWithSteps,
    ticket: TicketData,
    settings: WorkflowSettings
  ): Promise<number> {
    const execResult = await postgresClient.query<{ id: number }>(
      `INSERT INTO ticket_workflow_executions (workflow_id, ticket_id, ticket_number, status)
       VALUES ($1, $2, $3, 'running')
       RETURNING id`,
      [workflow.id, ticket.id, ticket.ticket_number]
    );
    const executionId = execResult.rows[0].id;

    const context: WorkflowStepContext = {
      ticket,
      _settings: settings,
      field_changes: {},
    };

    let finalStatus: 'pending' | 'running' | 'completed' | 'failed' | 'skipped' = 'completed';
    let classificationMethod: 'robotic' | 'ai' | 'hybrid' = 'robotic';
    let branch: 'service_desk' | 'noc' | 'soc' = 'service_desk';
    let errorMessage: string | null = null;

    console.log(`[TICKET-WORKFLOW] Executing "${workflow.name}" (exec #${executionId}), ${workflow.steps.length} steps`);

    for (const step of workflow.steps) {
      // Check step condition
      if (step.condition && !this.evaluateStepCondition(step.condition, context)) {
        console.log(`[TICKET-WORKFLOW] Step ${step.step_order} "${step.name}" skipped (condition not met)`);
        await this.logStepExecution(executionId, step, 'skipped', null, null, 0, 'Condition not met');
        continue;
      }

      const stepStart = Date.now();

      // Log step start
      await this.logStepExecution(executionId, step, 'running', { config: step.config }, null, 0);

      const executor = workflowStepExecutors.get(step.step_type);
      if (!executor) {
        const err = `No executor registered for step type: ${step.step_type}`;
        console.error(`[TICKET-WORKFLOW] ${err}`);
        await this.logStepExecution(executionId, step, 'failed', null, null, Date.now() - stepStart, err);

        if (step.on_failure === 'stop') {
          finalStatus = 'failed';
          errorMessage = err;
          break;
        }
        continue;
      }

      try {
        // Resolve template variables in step config
        const resolvedConfig = this.resolveTemplates(step.config, context);
        const resolvedStep = { ...step, config: resolvedConfig };

        const result = await executor(resolvedStep, context, executionId);
        const duration = Date.now() - stepStart;

        if (result.success) {
          // Merge output into context
          if (result.output) {
            Object.assign(context, result.output);

            // Track if AI was used
            if (result.output.method === 'ai') {
              classificationMethod = classificationMethod === 'robotic' ? 'hybrid' : 'ai';
            }
          }

          await this.logStepExecution(executionId, step, 'completed', null, result.output, duration);
          console.log(`[TICKET-WORKFLOW] Step ${step.step_order} "${step.name}" completed (${duration}ms)`);
        } else {
          await this.logStepExecution(executionId, step, 'failed', null, result.output, duration, result.error || null);
          console.error(`[TICKET-WORKFLOW] Step ${step.step_order} "${step.name}" failed: ${result.error}`);

          if (step.on_failure === 'stop') {
            finalStatus = 'failed';
            errorMessage = `Step ${step.step_order} "${step.name}": ${result.error}`;
            break;
          } else if (step.on_failure === 'skip_to' && step.skip_to_step) {
            // Skip ahead (simplified: just continue, full implementation would jump to specific step)
            continue;
          }
          // on_failure === 'continue' → keep going
        }
      } catch (err) {
        const errMsg = err instanceof Error ? err.message : String(err);
        const duration = Date.now() - stepStart;
        await this.logStepExecution(executionId, step, 'failed', null, null, duration, errMsg);
        console.error(`[TICKET-WORKFLOW] Step ${step.step_order} "${step.name}" threw: ${errMsg}`);

        if (step.on_failure === 'stop') {
          finalStatus = 'failed';
          errorMessage = errMsg;
          break;
        }
      }
    }

    // Extract branch from context if set
    if (context.classification?.branch_routing) {
      branch = (context.classification.branch_routing.value as any) || 'service_desk';
    }

    // Finalize execution
    await postgresClient.query(
      `UPDATE ticket_workflow_executions
       SET status = $1, classification_method = $2, branch = $3, context = $4,
           field_changes = $5, completed_at = NOW(),
           duration_ms = EXTRACT(EPOCH FROM (NOW() - started_at)) * 1000,
           error_message = $6
       WHERE id = $7`,
      [
        finalStatus,
        classificationMethod,
        branch,
        JSON.stringify(context),
        JSON.stringify(context.field_changes || {}),
        errorMessage,
        executionId
      ]
    );

    console.log(`[TICKET-WORKFLOW] Execution #${executionId} finished: ${finalStatus}`);
    return executionId;
  }

  /**
   * Dry-run: execute a workflow on a ticket without actually updating Autotask.
   */
  async dryRun(workflowId: number, ticketId: number): Promise<any> {
    // Load workflow
    const workflowResult = await postgresClient.query<TicketWorkflow>(
      `SELECT * FROM ticket_workflows WHERE id = $1`,
      [workflowId]
    );

    if (workflowResult.rows.length === 0) {
      throw new Error(`Workflow #${workflowId} not found`);
    }

    const workflow = workflowResult.rows[0];

    // Load steps
    const stepsResult = await postgresClient.query<TicketWorkflowStep>(
      `SELECT * FROM ticket_workflow_steps
       WHERE workflow_id = $1 AND is_active = true
       ORDER BY step_order`,
      [workflowId]
    );

    const workflowWithSteps: TicketWorkflowWithSteps = {
      ...workflow,
      steps: stepsResult.rows
    };

    // Load ticket
    const ticketResult = await postgresClient.query<TicketData>(
      `SELECT * FROM tickets WHERE id = $1`,
      [ticketId]
    );

    if (ticketResult.rows.length === 0) {
      throw new Error(`Ticket #${ticketId} not found`);
    }

    const ticket = ticketResult.rows[0];

    // Load settings
    const settings = await this.getSettings();

    // Execute workflow (will create a real execution record)
    // For dry-run, we could skip the update_ticket step or mark it differently
    // For now, we'll execute normally but return the execution ID for inspection
    const executionId = await this.executeWorkflow(workflowWithSteps, ticket, settings);

    // Fetch execution result
    const execResult = await postgresClient.query<TicketWorkflowExecution>(
      `SELECT * FROM ticket_workflow_executions WHERE id = $1`,
      [executionId]
    );

    // Fetch execution steps
    const stepsExecResult = await postgresClient.query(
      `SELECT * FROM ticket_workflow_execution_steps WHERE execution_id = $1 ORDER BY step_order`,
      [executionId]
    );

    return {
      execution: execResult.rows[0],
      steps: stepsExecResult.rows
    };
  }

  /**
   * Evaluate trigger conditions (AND logic).
   */
  private evaluateTriggerConditions(conditions: TriggerCondition[], ticket: TicketData): boolean {
    for (const condition of conditions) {
      const value = (ticket as any)[condition.field];

      switch (condition.operator) {
        case 'equals':
          if (value !== condition.value) return false;
          break;
        case 'not_equals':
          if (value === condition.value) return false;
          break;
        case 'in':
          if (!Array.isArray(condition.value) || !condition.value.includes(value)) return false;
          break;
        case 'not_in':
          if (!Array.isArray(condition.value) || condition.value.includes(value)) return false;
          break;
        case 'contains':
          if (typeof value !== 'string' || !value.includes(String(condition.value))) return false;
          break;
        case 'not_contains':
          if (typeof value === 'string' && value.includes(String(condition.value))) return false;
          break;
        case 'gt':
          if (!(Number(value) > Number(condition.value))) return false;
          break;
        case 'lt':
          if (!(Number(value) < Number(condition.value))) return false;
          break;
        default:
          return false;
      }
    }
    return true;
  }

  /**
   * Evaluate step condition.
   */
  private evaluateStepCondition(condition: StepCondition, context: WorkflowStepContext): boolean {
    const value = this.getNestedValue(context, condition.field);

    switch (condition.operator) {
      case 'equals':
        return value === condition.value;
      case 'not_equals':
        return value !== condition.value;
      case 'in':
        return Array.isArray(condition.value) && condition.value.includes(value);
      case 'not_in':
        return Array.isArray(condition.value) && !condition.value.includes(value);
      case 'contains':
        if (typeof value === 'string') {
          return value.includes(String(condition.value));
        }
        if (Array.isArray(value)) {
          return value.some(v => String(v).includes(String(condition.value)));
        }
        return false;
      case 'not_contains':
        if (typeof value === 'string') {
          return !value.includes(String(condition.value));
        }
        return true;
      case 'gt':
        return Number(value) > Number(condition.value);
      case 'lt':
        return Number(value) < Number(condition.value);
      case 'is_null':
        return value === null || value === undefined;
      case 'is_not_null':
        return value !== null && value !== undefined;
      default:
        return false;
    }
  }

  /**
   * Resolve template variables in config (supports {{context.field}} syntax).
   */
  private resolveTemplates(value: any, context: WorkflowStepContext): any {
    if (typeof value === 'string') {
      return this.resolveStringTemplate(value, context);
    }
    if (Array.isArray(value)) {
      return value.map(v => this.resolveTemplates(v, context));
    }
    if (value && typeof value === 'object') {
      const resolved: Record<string, any> = {};
      for (const [k, v] of Object.entries(value)) {
        resolved[k] = this.resolveTemplates(v, context);
      }
      return resolved;
    }
    return value;
  }

  private resolveStringTemplate(template: string, context: WorkflowStepContext): string {
    return template.replace(/\{\{([^}]+)\}\}/g, (match, path: string) => {
      const trimmedPath = path.trim();

      // Handle special "settings.*" paths
      if (trimmedPath.startsWith('settings.')) {
        const settingKey = trimmedPath.substring('settings.'.length);
        const value = (context._settings as any)[settingKey];
        return value !== undefined && value !== null ? String(value) : '';
      }

      // Handle "context.*" paths
      if (trimmedPath.startsWith('context.')) {
        const contextKey = trimmedPath.substring('context.'.length);
        const value = this.getNestedValue(context, contextKey);
        return value !== undefined && value !== null ? String(value) : '';
      }

      // Direct context access
      const value = this.getNestedValue(context, trimmedPath);
      return value !== undefined && value !== null ? String(value) : '';
    });
  }

  private getNestedValue(obj: any, path: string): any {
    const parts = path.split('.');
    let current = obj;
    for (const part of parts) {
      if (current == null) return undefined;
      current = current[part];
    }
    return current;
  }

  /**
   * Log step execution to database.
   */
  private async logStepExecution(
    executionId: number,
    step: TicketWorkflowStep,
    status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped',
    inputData: any,
    outputData: any,
    durationMs: number,
    errorMessage?: string | null
  ): Promise<void> {
    if (status === 'running') {
      await postgresClient.query(
        `INSERT INTO ticket_workflow_execution_steps
         (execution_id, step_order, step_type, step_name, status, started_at, input_data)
         VALUES ($1, $2, $3, $4, $5, NOW(), $6)`,
        [executionId, step.step_order, step.step_type, step.name, status, JSON.stringify(inputData)]
      );
    } else {
      await postgresClient.query(
        `UPDATE ticket_workflow_execution_steps
         SET status = $1, output_data = $2, completed_at = NOW(), duration_ms = $3, error_message = $4
         WHERE execution_id = $5 AND step_order = $6`,
        [status, JSON.stringify(outputData), durationMs, errorMessage, executionId, step.step_order]
      );
    }
  }
}

// Export singleton instance
export const ticketWorkflowEngine = new TicketWorkflowEngine();
