import "dotenv/config";
import express from "express";
import { Request, Response } from "express";
import { createOrchestratorRoutes } from "./routes/orchestratorRoutes";
import { AlchemyWebhookAddressService } from "./services/AlchemyWebhookAddressService";
import { AlchemyMultiChainService } from "./services/AlchemyMultiChainService";
import { AlchemyTrackingStoreService } from "./services/AlchemyTrackingStoreService";
import { AlchemyWebhookNormalizationService } from "./services/AlchemyWebhookNormalizationService";
import { AlchemyWebhookVerificationService } from "./services/AlchemyWebhookVerificationService";
import { AlchemyWebhookPayloadValidator } from "./services/AlchemyWebhookPayloadValidator";
import { AlchemyTransfersClient } from "./services/AlchemyTransfersClient";
import { DepositQueueService } from "./services/DepositQueueService";
import { DepositReconciliationService } from "./services/DepositReconciliationService";
import { DepositWorkerService } from "./services/DepositWorkerService";
import { LaravelDepositClient } from "./services/LaravelDepositClient";
import { MappingStoreService } from "./services/MappingStoreService";
import { MultiChainSweeperService } from "./services/MultiChainSweeperService";
import { ensurePostgresControlStore, PostgresMappingStore, PostgresTrackingStore } from "./services/PostgresControlStore";
import { PostgresDepositQueueStore, PostgresReconciliationCursorStore, createPostgresStateStore } from "./services/PostgresStateStore";
import { ReconciliationStoreService } from "./services/ReconciliationStoreService";
import { IDepositQueueStore, IReconciliationCursorStore } from "./services/StateStoreContracts";
import { IBlinkInvoiceStore, IMappingStore, ITrackingStore } from "./services/StoreContracts";
import { WalletGenerationService } from "./services/WalletGenerationService";
import { WalletRegistrationService } from "./services/WalletRegistrationService";
import { AlchemyDepositScannerAdapter } from "./providers/alchemy/AlchemyDepositScannerAdapter";
import { AlchemyWebhookProcessorAdapter } from "./providers/alchemy/AlchemyWebhookProcessorAdapter";
import { QuickNodeDepositScannerAdapter } from "./providers/quicknode/QuickNodeDepositScannerAdapter";
import { MultiProviderDepositScanner } from "./providers/MultiProviderDepositScanner";
import { BlinkClient } from "./providers/blink/BlinkClient";
import { BlinkInvoiceService } from "./providers/blink/BlinkInvoiceService";
import { BlinkWebhookProcessor } from "./providers/blink/BlinkWebhookProcessor";
import { createBlinkRoutes } from "./routes/blinkRoutes";
import { BlinkInvoiceStoreService } from "./services/BlinkInvoiceStoreService";
import { PostgresBlinkInvoiceStore } from "./services/PostgresStateStore";

const port = Number(process.env.PORT || 8010);
const webhookSigningKey = process.env.ALCHEMY_WEBHOOK_SIGNING_KEY?.trim();
const webhookSigningKeysRaw = process.env.ALCHEMY_WEBHOOK_SIGNING_KEYS?.trim();
const laravelBaseUrl = process.env.LARAVEL_BASE_URL;
const laravelRelayHmacSecret = process.env.LARAVEL_RELAY_HMAC_SECRET;
const mappingStorePath = process.env.MAPPING_STORE_PATH || "./data/mappings.json";
const trackingStorePath = process.env.TRACKING_STORE_PATH || "./data/alchemy-tracking.json";
const blinkInvoiceStorePath = process.env.BLINK_INVOICE_STORE_PATH || "./data/blink-invoices.json";
const relayBufferPath =
  process.env.EVENT_QUEUE_PATH || process.env.RELAY_BUFFER_PATH || "./data/relay-queue.json";
const reconciliationStorePath =
  process.env.RECONCILIATION_STORE_PATH || "./data/reconciliation-state.json";
const orchestratorDatabaseUrl =
  process.env.ORCHESTRATOR_DATABASE_URL ||
  process.env.DATABASE_URL ||
  "";
const relayFlushIntervalMs = Number(process.env.RELAY_FLUSH_INTERVAL_MS || 30000);
const reconciliationIntervalMs = Number(process.env.RECONCILIATION_INTERVAL_MS || 300000);
const deliveredRetentionDays = Number(process.env.EVENT_DELIVERED_RETENTION_DAYS || 30);
const defaultWebhookCapacity = Number(process.env.ALCHEMY_WEBHOOK_ADDRESS_LIMIT || 100000);
const reconciliationLookbackBlocks = Number(process.env.RECONCILIATION_LOOKBACK_BLOCKS || 5000);
const reconciliationBatchSize = Number(process.env.RECONCILIATION_BATCH_SIZE || 100);

function toErrorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

// Parse webhook signing keys - support both single key (backward compatibility) and multiple keys
let webhookSigningKeys: Record<string, string> | string;
if (webhookSigningKeysRaw) {
  try {
    webhookSigningKeys = JSON.parse(webhookSigningKeysRaw);
  } catch {
    throw new Error("ALCHEMY_WEBHOOK_SIGNING_KEYS must be valid JSON");
  }
} else if (webhookSigningKey) {
  // Backward compatibility: use single signing key
  webhookSigningKeys = webhookSigningKey;
} else {
  throw new Error("Missing required environment variables: ALCHEMY_WEBHOOK_SIGNING_KEYS or ALCHEMY_WEBHOOK_SIGNING_KEY");
}

if (!laravelBaseUrl) {
  throw new Error("Missing required environment variable: LARAVEL_BASE_URL");
}

const app = express();
app.use(
  express.json({
    limit: "1mb",
    verify: (request, _response, buffer) => {
      (request as Request & { rawBody?: string }).rawBody = buffer.toString("utf8");
    },
  })
);

const laravelDepositClient = new LaravelDepositClient(
  laravelBaseUrl,
  process.env.LARAVEL_RELAY_SECRET,
  laravelRelayHmacSecret
);
async function bootstrap() {
  let mappingStoreService: IMappingStore = new MappingStoreService(mappingStorePath);
  let trackingStoreService: ITrackingStore = new AlchemyTrackingStoreService(trackingStorePath);
  let blinkInvoiceStore: IBlinkInvoiceStore = new BlinkInvoiceStoreService(blinkInvoiceStorePath);

  let depositQueueService: IDepositQueueStore = new DepositQueueService(
    relayBufferPath,
    deliveredRetentionDays
  );
  let reconciliationStoreService: IReconciliationCursorStore = new ReconciliationStoreService(reconciliationStorePath);

  if (orchestratorDatabaseUrl) {
    const pool = await createPostgresStateStore(orchestratorDatabaseUrl);
    await ensurePostgresControlStore(pool);
    mappingStoreService = new PostgresMappingStore(pool);
    trackingStoreService = new PostgresTrackingStore(pool);
    blinkInvoiceStore = new PostgresBlinkInvoiceStore(pool);
    depositQueueService = new PostgresDepositQueueStore(pool, deliveredRetentionDays);
    reconciliationStoreService = new PostgresReconciliationCursorStore(pool);
    console.log("orchestrator state store: postgres");
  } else {
    console.log("orchestrator state store: file");
  }

  const alchemyWebhookAddressService = new AlchemyWebhookAddressService(
    process.env.ALCHEMY_AUTH_TOKEN,
    process.env.ALCHEMY_WEBHOOK_SHARDS,
    trackingStoreService,
    defaultWebhookCapacity
  );
  await alchemyWebhookAddressService.initialize();
  const walletRegistrationService = new WalletRegistrationService(
    mappingStoreService,
    alchemyWebhookAddressService
  );
  const walletGenerationService = new WalletGenerationService(
    process.env.WALLET_ENGINE_MNEMONIC,
    process.env.WALLET_ENGINE_EVM_DERIVATION_PATH,
    process.env.WALLET_ENGINE_SOL_DERIVATION_PATH
  );
  const alchemyMultiChainService = new AlchemyMultiChainService(
    process.env.ALCHEMY_RPC_URLS,
    mappingStoreService,
    walletRegistrationService,
    walletGenerationService
  );
  const multiChainSweeperService = new MultiChainSweeperService(
    process.env.ALCHEMY_RPC_URLS,
    process.env.SWEEPER_DESTINATION_ADDRESSES,
    process.env.SWEEPER_NATIVE_MIN_RESERVE,
    process.env.SWEEPER_FEE_FUNDING_ADDRESSES,
    process.env.SWEEPER_FEE_FUNDING_PRIVATE_KEYS,
    mappingStoreService,
    walletGenerationService
  );
  const webhookVerificationService = new AlchemyWebhookVerificationService(webhookSigningKeys);
  const normalizationService = new AlchemyWebhookNormalizationService(
    mappingStoreService,
    process.env.SOLANA_MINT_SYMBOLS,
    process.env.SWEEPER_FEE_FUNDING_ADDRESSES
  );
  const webhookProcessor = new AlchemyWebhookProcessorAdapter(
    webhookVerificationService,
    new AlchemyWebhookPayloadValidator(),
    normalizationService
  );
  const blinkClient = new BlinkClient({
    apiUrl: process.env.BLINK_API_URL || "https://api.blink.sv/graphql",
    apiKey: process.env.BLINK_API_KEY || "",
  });
  const blinkInvoiceService = new BlinkInvoiceService(blinkClient, blinkInvoiceStore, {
    btcWalletId: process.env.BLINK_BTC_WALLET_ID,
    defaultExpiresInMinutes: process.env.BLINK_INVOICE_EXPIRES_IN_MINUTES
      ? Number(process.env.BLINK_INVOICE_EXPIRES_IN_MINUTES)
      : undefined,
  });

  const depositWorkerService = new DepositWorkerService(depositQueueService, laravelDepositClient);
  const blinkWebhookProcessor = new BlinkWebhookProcessor(blinkInvoiceStore);
  const alchemyTransfersClient = new AlchemyTransfersClient(process.env.ALCHEMY_RPC_URLS);
  const depositScannerAdapter = new AlchemyDepositScannerAdapter(
    alchemyTransfersClient,
    mappingStoreService
  );
  const quickNodeScannerAdapter = new QuickNodeDepositScannerAdapter(
    alchemyTransfersClient,
    mappingStoreService
  );
  const depositScanner = new MultiProviderDepositScanner(
    {
      alchemy: depositScannerAdapter,
      quicknode: quickNodeScannerAdapter,
    },
    process.env.DEPOSIT_SCANNER_PROVIDER_BY_CHAIN,
    process.env.DEPOSIT_SCANNER_DEFAULT_PROVIDER || "alchemy"
  );
  const depositReconciliationService = new DepositReconciliationService(
    trackingStoreService,
    depositScanner,
    depositQueueService,
    reconciliationStoreService,
    reconciliationBatchSize,
    reconciliationLookbackBlocks
  );

  app.get("/health", (_: Request, response: Response) => {
    response.status(200).json({ status: 200, message: "ok" });
  });

  app.use(
    "/",
    createBlinkRoutes(
      blinkInvoiceService,
      blinkWebhookProcessor,
      depositQueueService,
      depositWorkerService
    )
  );

  app.use(
    "/v1/orchestrator",
    createBlinkRoutes(
      blinkInvoiceService,
      blinkWebhookProcessor,
      depositQueueService,
      depositWorkerService
    )
  );

  app.use(
    "/v1/orchestrator",
    createOrchestratorRoutes(
      walletRegistrationService,
      alchemyMultiChainService,
      multiChainSweeperService,
      webhookProcessor,
      depositQueueService,
      depositWorkerService
    )
  );

  const flushInterval = setInterval(() => {
    depositWorkerService.processBatch().catch((error: unknown) => {
      console.error("deposit worker batch failed", toErrorMessage(error));
    });
  }, relayFlushIntervalMs);

  const reconciliationInterval = setInterval(() => {
    depositReconciliationService.reconcile().catch((error: unknown) => {
      console.error("deposit reconciliation failed", toErrorMessage(error));
    });
  }, reconciliationIntervalMs);

  let server = app.listen(port, () => {
    console.log(`wallet_orchestrator listening on ${port}`);
  });

  server.on("error", (error: any) => {
    if (error?.code === "EADDRINUSE") {
      console.warn(`port ${port} is busy, retrying...`);
      setTimeout(() => {
        server = app.listen(port, () => {
          console.log(`wallet_orchestrator listening on ${port}`);
        });
      }, 700);
      return;
    }

    throw error;
  });

  const shutdown = () => {
    clearInterval(flushInterval);
    clearInterval(reconciliationInterval);
    server.close(() => {
      process.exit(0);
    });
  };

  process.once("SIGINT", shutdown);
  process.once("SIGTERM", shutdown);
  process.once("SIGUSR2", shutdown);
}

bootstrap().catch((error) => {
  console.error("failed to bootstrap orchestrator", toErrorMessage(error));
  process.exit(1);
});
