import { resolve } from 'path';
import type { Server } from 'http';
import type net from 'net';
import { matchResources, currencyMatchesCwAsset } from './matchers/resourceMatcher';
import { parseInbound } from './parsers/inboundParser';
import { mapToCanonical } from './canonical/mapToCanonical';
import { buildResourceRegistry, loadListenerConfig } from './resourceRegistry';
import { buildEvent, EventStore } from './store/eventStore';
import {
  archiveInboxFile,
  pollFinGatewayMessages,
  pollP2pRailTransactions,
  scanDirectoryInbox,
  scanFileInbox,
} from './adapters/inboundAdapters';
import { createWebhookServer } from './adapters/webhookServer';
import { startSwiftFinTcpServer } from './adapters/swiftFinTcpAdapter';
import { dispatchMatchedOutbounds } from './outbound/dispatchOutcomes';
import type { InboundMessage, SwiftListenerEvent, SwiftListenerOptions } from './types';

export type { SwiftListenerOptions };

export class SwiftListener {
  private readonly config;
  private readonly registry;
  private readonly store: EventStore;
  private lastP2pId?: string;
  private lastFinId?: string;
  private running = false;
  private webhookServer?: Server;
  private tcpServer?: net.Server;

  constructor(private readonly projectRoot: string) {
    this.config = loadListenerConfig(this.projectRoot);
    this.registry = buildResourceRegistry(this.projectRoot, this.config);
    this.store = new EventStore(this.projectRoot, this.config);
  }

  async processMessage(msg: InboundMessage): Promise<SwiftListenerEvent | null> {
    const dedupeKey = this.store.dedupeKey(msg.source, msg.payload);
    if (this.store.isProcessed(dedupeKey)) return null;

    const parsed = parseInbound(msg.payload);
    let matches = matchResources(this.registry, parsed);

    const currency =
      typeof parsed.parsed.currency === 'string' ? parsed.parsed.currency : undefined;
    if (currencyMatchesCwAsset(currency, this.registry)) {
      matches = [
        ...matches,
        {
          kind: 'cw_symbol',
          value: currency!.toUpperCase(),
          symbol: currency!.toUpperCase(),
          field: 'parsed.currency',
        },
      ];
    }

    const canonical = matches.length > 0 ? mapToCanonical(parsed, dedupeKey) : undefined;
    let event = buildEvent(
      msg.source,
      parsed.parsed,
      parsed.format,
      String(parsed.messageType),
      matches,
      canonical,
      msg.payload,
      dedupeKey
    );

    if (event.matched && event.canonical) {
      event = {
        ...event,
        outbound: await dispatchMatchedOutbounds(
          this.projectRoot,
          this.config,
          this.registry,
          event
        ),
      };
      console.log(
        JSON.stringify({
          wired: 'match-outbound',
          eventId: event.id,
          omnlApi: event.outbound?.omnl?.apiPath,
          omnlStatus: event.outbound?.omnl?.apiStatus,
          gateway: event.outbound?.intakeGateway?.gateway,
          gatewayDryRun: event.outbound?.intakeGateway?.dryRun,
          gatewayTx: event.outbound?.intakeGateway?.txHash,
        })
      );
    }

    this.store.saveEvent(event);
    this.maybeArchiveSource(msg);
    return event;
  }

  private maybeArchiveSource(msg: InboundMessage): void {
    const filePath = msg.metadata?.filePath;
    if (typeof filePath !== 'string') return;

    const archiveDir = resolve(this.projectRoot, this.config.storage.archiveDir);
    if (msg.source.startsWith('file:') && this.config.adapters.fileInbox.archiveOnProcess) {
      try {
        archiveInboxFile(filePath, archiveDir);
      } catch {
        // inbox file may already be moved
      }
    }
    if (msg.source.startsWith('as4:') && this.config.adapters.as4Inbox?.archiveOnProcess) {
      try {
        archiveInboxFile(filePath, resolve(archiveDir, 'as4'));
      } catch {
        // ignore
      }
    }
    if (msg.source.startsWith('mail:') && this.config.adapters.mailAttachmentInbox?.archiveOnProcess) {
      try {
        archiveInboxFile(filePath, resolve(archiveDir, 'mail'));
      } catch {
        // ignore
      }
    }
    if (msg.source.startsWith('telex:') && this.config.adapters.telexInbox?.archiveOnProcess) {
      try {
        archiveInboxFile(filePath, resolve(archiveDir, 'telex'));
      } catch {
        // ignore
      }
    }
  }

  async processBatch(sources: InboundMessage[]): Promise<SwiftListenerEvent[]> {
    const out: SwiftListenerEvent[] = [];
    for (const msg of sources) {
      const ev = await this.processMessage(msg);
      if (ev) out.push(ev);
    }
    return out;
  }

  async pollFileInbox(): Promise<SwiftListenerEvent[]> {
    return this.processBatch(scanFileInbox(this.store.inboxPath()));
  }

  async pollAs4Inbox(): Promise<SwiftListenerEvent[]> {
    const dir = this.config.storage.as4InboxDir;
    if (!dir) return [];
    return this.processBatch(
      scanDirectoryInbox(resolve(this.projectRoot, dir), 'as4', 'iso20022')
    );
  }

  async pollMailInbox(): Promise<SwiftListenerEvent[]> {
    const dir = this.config.storage.mailInboxDir;
    if (!dir) return [];
    return this.processBatch(scanDirectoryInbox(resolve(this.projectRoot, dir), 'mail'));
  }

  async pollTelexInbox(): Promise<SwiftListenerEvent[]> {
    const dir = this.config.storage.telexInboxDir;
    if (!dir) return [];
    return this.processBatch(scanDirectoryInbox(resolve(this.projectRoot, dir), 'telex', 'ktt'));
  }

  async pollP2pRail(): Promise<SwiftListenerEvent[]> {
    const cfg = this.config.adapters.p2pRail;
    const baseUrl = process.env[cfg.baseUrlEnv] ?? process.env.P2P_BASE_URL ?? '';
    if (!baseUrl) return [];
    const messages = await pollP2pRailTransactions({
      baseUrl,
      bearerToken: process.env.P2P_BEARER_TOKEN,
      apiKey: process.env.P2P_API_KEY,
      path: cfg.transactionsPath,
      sinceId: this.lastP2pId,
    });
    this.trackLastId(messages, 'p2p');
    return this.processBatch(messages);
  }

  async pollFinGateway(): Promise<SwiftListenerEvent[]> {
    const cfg = this.config.adapters.finGateway;
    const baseUrl =
      process.env[cfg.baseUrlEnv] ??
      (cfg.fallbackBaseUrlEnv ? process.env[cfg.fallbackBaseUrlEnv] : undefined) ??
      process.env.ALLIANCE_ACCESS_URL ??
      '';
    if (!baseUrl) return [];
    const tokenEnv = cfg.authBearerEnv ? process.env[cfg.authBearerEnv] : process.env.FIN_GATEWAY_TOKEN;
    const messages = await pollFinGatewayMessages({
      baseUrl,
      bearerToken: tokenEnv,
      path: cfg.messagesPath,
      sinceId: this.lastFinId,
    });
    this.trackLastId(messages, 'fin');
    return this.processBatch(messages);
  }

  private trackLastId(messages: InboundMessage[], prefix: 'p2p' | 'fin'): void {
    if (messages.length === 0) return;
    const last = messages[messages.length - 1];
    const meta = last.metadata as Record<string, unknown> | undefined;
    const id = String(meta?.id ?? meta?.transactionId ?? meta?.messageId ?? '');
    if (prefix === 'p2p') this.lastP2pId = id || this.lastP2pId;
    else this.lastFinId = id || this.lastFinId;
  }

  private startLongRunningAdapters(): void {
    const { webhook, swiftFinTcp } = this.config.adapters;

    if (webhook.enabled) {
      const authEnv = webhook.authBearerEnv ?? 'SWIFT_LISTENER_WEBHOOK_TOKEN';
      const secretEnv = webhook.webhookSecretEnv ?? 'HYBX_WEBHOOK_SECRET';
      const webhookSecret = process.env[secretEnv]?.trim();
      this.webhookServer = createWebhookServer({
        port: webhook.port,
        path: webhook.path,
        healthPath: webhook.healthPath,
        authBearer: webhookSecret ? undefined : process.env[authEnv],
        webhookSecret: webhookSecret || undefined,
        webhookSignatureHeader: webhook.webhookSignatureHeader,
        onMessage: async (msg) => {
          await this.processMessage(msg);
        },
      });
      console.log(
        JSON.stringify({
          adapter: 'webhook',
          port: webhook.port,
          path: webhook.path,
          healthPath: webhook.healthPath ?? '/health',
        })
      );
    }

    if (swiftFinTcp.enabled) {
      const port = parseInt(
        process.env[swiftFinTcp.portEnv] ?? String(swiftFinTcp.defaultPort),
        10
      );
      this.tcpServer = startSwiftFinTcpServer({
        host: swiftFinTcp.host,
        port,
        onMessage: (msg) => {
          void this.processMessage(msg);
        },
      });
      console.log(JSON.stringify({ adapter: 'swiftFinTcp', host: swiftFinTcp.host, port }));
    }
  }

  private pollIntervals(): number[] {
    const { adapters } = this.config;
    const intervals: number[] = [];
    if (adapters.fileInbox.enabled) intervals.push(adapters.fileInbox.pollIntervalSec);
    if (adapters.p2pRail.enabled) intervals.push(adapters.p2pRail.pollIntervalSec);
    if (adapters.finGateway?.enabled) intervals.push(adapters.finGateway.pollIntervalSec);
    if (adapters.as4Inbox?.enabled) intervals.push(adapters.as4Inbox.pollIntervalSec);
    if (adapters.mailAttachmentInbox?.enabled) intervals.push(adapters.mailAttachmentInbox.pollIntervalSec);
    if (adapters.telexInbox?.enabled) intervals.push(adapters.telexInbox.pollIntervalSec);
    return intervals.length ? intervals : [60];
  }

  async runPollCycle(): Promise<SwiftListenerEvent[]> {
    const events: SwiftListenerEvent[] = [];
    const { adapters } = this.config;

    if (adapters.fileInbox.enabled) {
      events.push(...(await this.pollFileInbox()));
    }
    if (adapters.as4Inbox?.enabled) {
      events.push(...(await this.pollAs4Inbox()));
    }
    if (adapters.mailAttachmentInbox?.enabled) {
      events.push(...(await this.pollMailInbox()));
    }
    if (adapters.telexInbox?.enabled) {
      events.push(...(await this.pollTelexInbox()));
    }
    if (adapters.p2pRail.enabled) {
      try {
        events.push(...(await this.pollP2pRail()));
      } catch (e) {
        console.error('P2P rail poll error:', e instanceof Error ? e.message : e);
      }
    }
    if (adapters.finGateway?.enabled) {
      try {
        events.push(...(await this.pollFinGateway()));
      } catch (e) {
        console.error('FIN gateway poll error:', e instanceof Error ? e.message : e);
      }
    }

    const matched = events.filter((e) => e.matched);
    if (events.length > 0) {
      console.log(
        JSON.stringify({
          at: new Date().toISOString(),
          processed: events.length,
          matched: matched.length,
          types: [...new Set(events.map((e) => e.messageType))],
          outbound: matched.filter((e) => e.outbound).length,
        })
      );
    }
    return events;
  }

  async run(options: SwiftListenerOptions = {}): Promise<void> {
    if (this.running) return;
    this.running = true;

    this.startLongRunningAdapters();
    await this.runPollCycle();

    if (options.once) {
      this.shutdown();
      return;
    }

    const intervalSec = Math.min(...this.pollIntervals());
    const handle = setInterval(() => {
      void this.runPollCycle();
    }, intervalSec * 1000);

    process.on('SIGINT', () => {
      clearInterval(handle);
      this.shutdown();
      process.exit(0);
    });
  }

  shutdown(): void {
    this.webhookServer?.close();
    this.tcpServer?.close();
    this.running = false;
  }
}

export function createSwiftListener(projectRoot: string): SwiftListener {
  return new SwiftListener(projectRoot);
}
