import type {
  SendRequest,
  SendResponse,
  NormalizedMessage,
  AttemptRecord,
  LogMessageRecord,
  GatewayErrorCode,
} from '@zyphon/shared-types';
import { toAttachmentMeta } from '@zyphon/shared-types';
import type { ProviderAdapter, ProviderResult } from '@zyphon/provider-contracts';
import type { ApplicationRegistry } from '../core/application-registry.js';
import type { RoutingEngine } from '../core/routing-engine.js';
import type { CredentialVault } from '../core/credential-vault.js';
import type { CircuitBreakerManager } from '../core/circuit-breaker.js';
import type { IdempotencyManager } from '../core/idempotency-manager.js';
import type { ProviderRegistry } from '../core/provider-registry.js';
import type { DailyLogWriter } from '@zyphon/logger';
import type { EventBus } from '../realtime/event-bus.js';
import type { ConfigLoader } from '@zyphon/config-loader';
import { isDltAdapterType } from '../catalog/provider-catalog.js';
import { validateSmsDltDispatch } from '../compliance/dlt-validator.js';
import {
  generateMessageId,
  generateRequestId,
  validateRecipient,
  maskRecipient,
  normalizeMessage,
} from './normalizer.js';
import { applyMessageTemplate } from './template-renderer.js';

export interface DispatchOptions {
  apiKey?: string;
  idempotencyKey?: string;
  sync?: boolean;
  clientIp?: string;
}

export interface DispatchPipelineResult {
  statusCode: number;
  response: SendResponse;
  cacheHit?: boolean;
}

export class DispatchService {
  private appRegistry: ApplicationRegistry;
  private routingEngine: RoutingEngine;
  private vault: CredentialVault;
  private circuitBreaker: CircuitBreakerManager;
  private idempotencyManager: IdempotencyManager;
  private providerRegistry: ProviderRegistry;
  private logWriter: DailyLogWriter;
  private eventBus: EventBus;
  private configLoader?: ConfigLoader;
  private pendingAsyncQueueCount = 0;
  private readonly maxAsyncQueue = 5000;

  constructor(deps: {
    appRegistry: ApplicationRegistry;
    routingEngine: RoutingEngine;
    vault: CredentialVault;
    circuitBreaker: CircuitBreakerManager;
    idempotencyManager: IdempotencyManager;
    providerRegistry: ProviderRegistry;
    logWriter: DailyLogWriter;
    eventBus: EventBus;
    configLoader?: ConfigLoader;
  }) {
    this.appRegistry = deps.appRegistry;
    this.routingEngine = deps.routingEngine;
    this.vault = deps.vault;
    this.circuitBreaker = deps.circuitBreaker;
    this.idempotencyManager = deps.idempotencyManager;
    this.providerRegistry = deps.providerRegistry;
    this.logWriter = deps.logWriter;
    this.eventBus = deps.eventBus;
    this.configLoader = deps.configLoader;
  }

  public async dispatch(
    request: SendRequest,
    options: DispatchOptions = {}
  ): Promise<DispatchPipelineResult> {
    const startTime = Date.now();
    const requestId = generateRequestId();
    const messageId = generateMessageId();
    const sync = options.sync !== false; // Default true

    // STEP 1 & 2: Extract and authenticate API Key
    if (!options.apiKey) {
      return this.errorResult(401, 'UNAUTHENTICATED', 'Missing API key', requestId, messageId, request.channel);
    }

    const app = this.appRegistry.authenticateKey(options.apiKey);
    if (!app) {
      return this.errorResult(401, 'UNAUTHENTICATED', 'Invalid or expired API key', requestId, messageId, request.channel);
    }

    // STEP 3 & 4 & 6 & 7: Check App Status, Channel Authorization, Rate Limits & Quotas
    const validation = this.appRegistry.validateDispatch(app, request.channel);
    if (!validation.valid) {
      const statusMap: Record<string, number> = {
        APP_DISABLED: 403,
        CHANNEL_FORBIDDEN: 403,
        IP_FORBIDDEN: 403,
        RATE_LIMITED: 429,
        DAILY_QUOTA_EXCEEDED: 429,
      };
      const statusCode = statusMap[validation.code || ''] || 400;
      return this.errorResult(
        statusCode,
        (validation.code as GatewayErrorCode) || 'VALIDATION_FAILED',
        validation.message || 'Validation failed',
        requestId,
        messageId,
        request.channel,
        app.id
      );
    }

    const allowedIps = app.allowed_ips;
    if (allowedIps && allowedIps.length > 0) {
      const clientIp = normalizeClientIp(options.clientIp || '');
      if (!clientIp || !allowedIps.some((ip) => normalizeClientIp(ip) === clientIp)) {
        return this.errorResult(
          403,
          'IP_FORBIDDEN',
          'Request source IP is not allowed for this application',
          requestId,
          messageId,
          request.channel,
          app.id
        );
      }
    }

    // STEP 5: Idempotency Check
    if (options.idempotencyKey) {
      const cached = this.idempotencyManager.get(options.idempotencyKey);
      if (cached) {
        return {
          statusCode: 200,
          response: cached,
          cacheHit: true,
        };
      }
    }

    // STEP 8: Recipient Format Validation
    const recipientCheck = validateRecipient(request.channel, request.recipient);
    if (!recipientCheck.valid) {
      return this.errorResult(
        422,
        'INVALID_RECIPIENT',
        recipientCheck.error || 'Invalid recipient format',
        requestId,
        messageId,
        request.channel,
        app.id
      );
    }

    // STEP 9: Normalize Payload + optional template render
    let normalized = normalizeMessage(request, app.id, requestId, messageId);
    if (
      this.configLoader &&
      (normalized.content.type === 'template' || normalized.content.template)
    ) {
      let templates: ReturnType<ConfigLoader['loadTemplates']> = [];
      try {
        templates = this.configLoader.loadTemplates();
      } catch {
        templates = [];
      }
      const rendered = applyMessageTemplate(
        normalized.content,
        normalized.channel,
        templates,
        normalized.metadata,
      );
      if (!rendered.ok) {
        return this.errorResult(
          422,
          'VALIDATION_FAILED',
          rendered.message,
          requestId,
          messageId,
          request.channel,
          app.id,
        );
      }
      normalized = {
        ...normalized,
        content: rendered.content,
        metadata: rendered.metadata,
      };
    }

    // STEP 9a: Channel attachment rules (fail closed for Indian DLT SMS; require URL for Twilio MMS)
    const attachments = normalized.content.attachments || [];
    if (attachments.length > 0 && request.channel === 'sms') {
      const routePreview = this.routingEngine.resolveRoute(app, request.channel);
      const primaryProvider = routePreview.activeProviders[0];
      const def = primaryProvider ? this.providerRegistry.getDefinition(primaryProvider) : undefined;
      const adapterId = primaryProvider ? this.providerRegistry.getAdapter(primaryProvider)?.id : undefined;
      const type = String(def?.adapter || adapterId || '').toLowerCase();
      const isTwilioSms = type.includes('twilio');
      if (!isTwilioSms) {
        return this.errorResult(
          422,
          'VALIDATION_FAILED',
          'SMS attachments are not supported on Indian DLT / text-only SMS routes. Send files via WhatsApp or Email, or include a short link in the SMS body.',
          requestId,
          messageId,
          request.channel,
          app.id,
        );
      }
      if (!attachments.some((a) => Boolean(a.url && a.url.trim()))) {
        return this.errorResult(
          422,
          'VALIDATION_FAILED',
          'Twilio SMS MMS requires attachment.url (public HTTPS media URL). Base64-only attachments are not supported on SMS.',
          requestId,
          messageId,
          request.channel,
          app.id,
        );
      }
    }

    // STEP 9b: India DLT compliance for DLT-flagged SMS providers
    if (request.channel === 'sms' && this.configLoader) {
      const routePreview = this.routingEngine.resolveRoute(app, request.channel);
      const primaryProvider = routePreview.activeProviders[0];
      const adapterType =
        (primaryProvider && this.providerRegistry.getDefinition(primaryProvider)?.adapter) ||
        (primaryProvider && this.providerRegistry.getAdapter(primaryProvider)?.id) ||
        '';
      const def = primaryProvider ? this.providerRegistry.getDefinition(primaryProvider) : undefined;
      const type = def?.adapter || adapterType;
      if (type && isDltAdapterType(type)) {
        let template;
        const templateId =
          (normalized.metadata.zyphon_template_id as string | undefined) ||
          normalized.content.template;
        if (templateId) {
          try {
            template = this.configLoader
              .loadTemplates()
              .find((t) => t.id === templateId);
          } catch {
            template = undefined;
          }
        }
        let dlt;
        let suppressions: string[] = [];
        try {
          dlt = this.configLoader.loadDltConfig();
          suppressions = this.configLoader.loadSuppressions().sms || [];
        } catch (err: any) {
          return this.errorResult(
            503,
            'VALIDATION_FAILED',
            err.message || 'DLT configuration is invalid',
            requestId,
            messageId,
            request.channel,
            app.id,
          );
        }
        const dltResult = validateSmsDltDispatch(normalized, {
          dlt,
          template,
          suppressions,
          providerConfig: def?.config,
        });
        if (!dltResult.ok) {
          const statusCode = dltResult.code === 'BLOCKED' ? 403 : 422;
          return this.errorResult(
            statusCode,
            dltResult.code === 'BLOCKED' ? 'CHANNEL_FORBIDDEN' : 'VALIDATION_FAILED',
            dltResult.message,
            requestId,
            messageId,
            request.channel,
            app.id,
          );
        }
      }
    }

    // STEP 10: Routing Resolution
    const route = this.routingEngine.resolveRoute(app, request.channel);
    if (route.candidateProviders.length === 0) {
      return this.errorResult(
        503,
        'NO_PROVIDER',
        `No provider configured for channel '${request.channel}'`,
        requestId,
        messageId,
        request.channel,
        app.id
      );
    }

    // STEP 11: Circuit Breaker Evaluation
    if (route.activeProviders.length === 0) {
      return this.errorResult(
        503,
        'ALL_PROVIDERS_FAILED',
        `All providers for channel '${request.channel}' are currently open due to repeated failures`,
        requestId,
        messageId,
        request.channel,
        app.id
      );
    }

    // ASYNC MODE: If sync=false, accept immediately and process in background
    if (!sync) {
      if (this.pendingAsyncQueueCount >= this.maxAsyncQueue) {
        return this.errorResult(
          429,
          'QUEUE_FULL',
          'In-memory dispatch queue is full; please retry shortly',
          requestId,
          messageId,
          request.channel,
          app.id
        );
      }

      const acceptedResponse: SendResponse = {
        message_id: messageId,
        request_id: requestId,
        status: 'accepted',
        channel: request.channel,
        provider: route.activeProviders[0] || null,
        failover: false,
        attempts: [],
        provider_response: null,
        latency_ms: Date.now() - startTime,
      };

      if (options.idempotencyKey) {
        this.idempotencyManager.set(options.idempotencyKey, acceptedResponse);
      }

      this.pendingAsyncQueueCount++;
      setImmediate(async () => {
        try {
          await this.executeDispatchChain(normalized, route.activeProviders, route.timeoutMs, route.retryPolicy, app.id);
        } finally {
          this.pendingAsyncQueueCount--;
        }
      });

      return {
        statusCode: 202,
        response: acceptedResponse,
      };
    }

    // SYNC MODE (Default): Execute chain immediately
    const chainResult = await this.executeDispatchChain(
      normalized,
      route.activeProviders,
      route.timeoutMs,
      route.retryPolicy,
      app.id
    );

    if (options.idempotencyKey && chainResult.response.status === 'delivered') {
      this.idempotencyManager.set(options.idempotencyKey, chainResult.response);
    }

    return chainResult;
  }

  /**
   * STEP 12, 13, 14, 15: Decrypt credentials, execute provider chain, log, emit event.
   */
  private async executeDispatchChain(
    msg: NormalizedMessage,
    providers: string[],
    timeoutMs: number,
    retryPolicy: { max_attempts: number; backoff_ms: number },
    appId: string
  ): Promise<DispatchPipelineResult> {
    const chainStartTime = Date.now();
    const attempts: AttemptRecord[] = [];
    let successfulProvider: string | null = null;
    let providerResponse: Record<string, unknown> | null = null;
    let failover = false;

    for (let pIdx = 0; pIdx < providers.length; pIdx++) {
      const providerId = providers[pIdx]!;
      if (pIdx > 0) {
        failover = true;
      }

      const adapter = this.providerRegistry.getAdapter(providerId);
      if (!adapter) {
        attempts.push({
          provider: providerId,
          attempt_number: 1,
          ok: false,
          error: `Adapter for provider '${providerId}' not registered`,
          latency_ms: 0,
        });
        continue;
      }

      // STEP 12: Credential & Config Resolution (merges providers.yaml config + encrypted vault secrets)
      let creds: Record<string, any> = {};
      try {
        creds = this.providerRegistry.getResolvedCredentials(providerId);
      } catch (err: any) {
        attempts.push({
          provider: providerId,
          attempt_number: 1,
          ok: false,
          error: `Failed to decrypt credentials for '${providerId}': ${err.message}`,
          latency_ms: 0,
        });
        continue;
      }

      // STEP 13: Execute Provider Dispatch with Retry Policy
      let providerSuccess = false;
      const maxAttempts = Math.max(1, retryPolicy.max_attempts || 1);

      for (let attemptNum = 1; attemptNum <= maxAttempts; attemptNum++) {
        const attemptStart = Date.now();
        const controller = new AbortController();
        const timer = setTimeout(() => controller.abort(), timeoutMs);

        try {
          const result: ProviderResult = await adapter.send(msg, creds, controller.signal);
          clearTimeout(timer);
          const latency = Date.now() - attemptStart;

          if (result.success) {
            attempts.push({
              provider: providerId,
              attempt_number: attemptNum,
              ok: true,
              error: null,
              latency_ms: latency,
            });
            this.circuitBreaker.recordSuccess(providerId, latency);
            successfulProvider = providerId;
            providerResponse = result.raw_response || null;
            providerSuccess = true;
            break;
          } else {
            const errCode = result.error?.code || 'PROVIDER_5XX';
            const errMsg = result.error?.message || 'Provider failed';
            attempts.push({
              provider: providerId,
              attempt_number: attemptNum,
              ok: false,
              error: `[${errCode}] ${errMsg}`,
              latency_ms: latency,
            });

            // Check retryability: only TIMEOUT, PROVIDER_5XX, PROVIDER_UNAVAILABLE are retryable
            const isRetryable = ['TIMEOUT', 'PROVIDER_5XX', 'PROVIDER_UNAVAILABLE'].includes(errCode);
            if (isRetryable && attemptNum < maxAttempts) {
              const backoff = retryPolicy.backoff_ms * Math.pow(2, attemptNum - 1) + Math.floor(Math.random() * 50);
              await new Promise((resolve) => setTimeout(resolve, backoff));
            } else {
              this.circuitBreaker.recordFailure(providerId, latency, errMsg);
              break; // Try next provider in chain
            }
          }
        } catch (err: any) {
          clearTimeout(timer);
          const latency = Date.now() - attemptStart;
          const isTimeout = err.name === 'AbortError' || controller.signal.aborted;
          const errMsg = isTimeout ? 'Operation timed out' : (err.message || 'Internal adapter error');
          attempts.push({
            provider: providerId,
            attempt_number: attemptNum,
            ok: false,
            error: errMsg,
            latency_ms: latency,
          });

          this.circuitBreaker.recordFailure(providerId, latency, errMsg);
          if (isTimeout && attemptNum < maxAttempts) {
            const backoff = retryPolicy.backoff_ms * Math.pow(2, attemptNum - 1);
            await new Promise((resolve) => setTimeout(resolve, backoff));
          } else {
            break; // Try next provider
          }
        }
      }

      if (providerSuccess) {
        break; // Message delivered, exit chain
      }
    }

    const totalLatency = Date.now() - chainStartTime;
    const isSuccess = successfulProvider !== null;

    const response: SendResponse = {
      message_id: msg.message_id,
      request_id: msg.request_id,
      status: isSuccess ? 'delivered' : 'failed',
      channel: msg.channel,
      provider: successfulProvider,
      failover,
      attempts,
      provider_response: providerResponse,
      latency_ms: totalLatency,
      ...(isSuccess
        ? {}
        : {
            error: {
              code: 'ALL_PROVIDERS_FAILED',
              message: `All providers in chain failed for channel '${msg.channel}'`,
            },
          }),
    };

    // STEP 14: Log Generation (Daily Log, masked recipient, redacted secrets)
    const providerDisplayName = successfulProvider
      ? this.resolveProviderDisplayName(successfulProvider)
      : undefined;

    const logRecord: LogMessageRecord = {
      type: 'message',
      message_id: msg.message_id,
      request_id: msg.request_id,
      ts: Date.now(),
      application: appId,
      channel: msg.channel,
      provider: successfulProvider,
      ...(providerDisplayName ? { provider_name: providerDisplayName } : {}),
      status: response.status,
      latency_ms: totalLatency,
      attempts,
      provider_response: providerResponse,
      error: isSuccess ? null : { code: 'ALL_PROVIDERS_FAILED', message: 'All providers in chain failed' },
      recipient_masked: maskRecipient(msg.recipient),
      failover,
      ...(msg.content.subject ? { subject: msg.content.subject } : {}),
      ...(msg.content.body ? { body_preview: String(msg.content.body).slice(0, 600) } : {}),
      ...(toAttachmentMeta(msg.content.attachments)
        ? { attachments: toAttachmentMeta(msg.content.attachments) }
        : {}),
    };
    this.logWriter.write(logRecord);

    // STEP 15: Emit Real-Time SSE Event
    this.eventBus.publish('message_dispatched', {
      message_id: msg.message_id,
      request_id: msg.request_id,
      application: appId,
      channel: msg.channel,
      provider: successfulProvider,
      status: response.status,
      latency_ms: totalLatency,
      failover,
    });

    return {
      statusCode: isSuccess ? 200 : 502,
      response,
    };
  }

  private errorResult(
    statusCode: number,
    code: GatewayErrorCode,
    message: string,
    requestId: string,
    messageId: string,
    channel: any,
    appId = 'unknown'
  ): DispatchPipelineResult {
    const response: SendResponse = {
      message_id: messageId,
      request_id: requestId,
      status: 'failed',
      channel,
      provider: null,
      failover: false,
      attempts: [],
      provider_response: null,
      latency_ms: 0,
      error: { code, message },
    };

    // Log rejected/failed attempt
    this.logWriter.write({
      type: 'message',
      message_id: messageId,
      request_id: requestId,
      ts: Date.now(),
      application: appId,
      channel,
      provider: null,
      status: 'failed',
      latency_ms: 0,
      attempts: [],
      provider_response: null,
      error: { code, message },
      recipient_masked: 'unknown',
      failover: false,
    });

    return { statusCode, response };
  }

  private resolveProviderDisplayName(providerId: string): string {
    try {
      const conf = (this.configLoader?.loadProvidersFile() || []).find(
        (p: any) => p.id === providerId,
      ) as { name?: string } | undefined;
      if (conf?.name) return conf.name;
    } catch {}
    const def = this.providerRegistry.getDefinition(providerId) as { name?: string } | undefined;
    return def?.name || providerId;
  }

  public getPendingAsyncCount(): number {
    return this.pendingAsyncQueueCount;
  }

  public async drainPendingDispatches(timeoutMs = 30_000): Promise<void> {
    const deadline = Date.now() + timeoutMs;
    while (this.pendingAsyncQueueCount > 0 && Date.now() < deadline) {
      await new Promise((resolve) => setTimeout(resolve, 50));
    }
  }
}

function normalizeClientIp(ip: string): string {
  const trimmed = ip.trim();
  if (trimmed.startsWith('::ffff:')) {
    return trimmed.slice(7);
  }
  return trimmed;
}

