import "dotenv/config";
import { existsSync, readFileSync } from "fs";
import { createPostgresStateStore } from "../services/PostgresStateStore";
import { ensurePostgresControlStore } from "../services/PostgresControlStore";
import {
  AddressTrackingStore,
  DepositEvent,
  ReconciliationStore,
  UserMapping,
} from "../types";

interface MappingStoreFile {
  users: Record<string, UserMapping>;
}

interface DepositQueueFile {
  queue: Array<DepositEvent & {
    attempts: number;
    queued_at: string;
    updated_at: string;
    next_attempt_at: string;
    last_error?: string;
  }>;
  delivered: Record<string, string>;
}

function readJsonFile<T>(filePath: string, fallback: T): T {
  if (!filePath || !existsSync(filePath)) {
    return fallback;
  }

  return JSON.parse(readFileSync(filePath, "utf8")) as T;
}

function normalizeAddress(address: string): string {
  const trimmed = String(address || "").trim();
  return trimmed.startsWith("0x") || trimmed.startsWith("0X") ? trimmed.toLowerCase() : trimmed;
}

async function main() {
  const databaseUrl =
    process.env.ORCHESTRATOR_DATABASE_URL ||
    process.env.DATABASE_URL ||
    "";

  if (!databaseUrl) {
    throw new Error("Missing ORCHESTRATOR_DATABASE_URL or DATABASE_URL");
  }

  const mappingStorePath = process.env.MAPPING_STORE_PATH || "./data/mappings.json";
  const trackingStorePath = process.env.TRACKING_STORE_PATH || "./data/alchemy-tracking.json";
  const relayQueuePath =
    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 mappingStore = readJsonFile<MappingStoreFile>(mappingStorePath, { users: {} });
  const trackingStore = readJsonFile<AddressTrackingStore>(trackingStorePath, {
    version: 1,
    addresses: {},
    webhooks: {},
  });
  const depositQueueStore = readJsonFile<DepositQueueFile>(relayQueuePath, {
    queue: [],
    delivered: {},
  });
  const reconciliationStore = readJsonFile<ReconciliationStore>(reconciliationStorePath, {
    version: 1,
    cursors: {},
  });

  const pool = await createPostgresStateStore(databaseUrl);
  await ensurePostgresControlStore(pool);
  let skippedTrackedAddresses = 0;

  const client = await pool.connect();
  try {
    await client.query("BEGIN");

    for (const user of Object.values(mappingStore.users || {})) {
      for (const wallet of user.wallets || []) {
        await client.query(
          `INSERT INTO user_wallet_mappings
            (user_id, chain_type, address, normalized_address, wallet_id, created_at, updated_at)
           VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
           ON CONFLICT (user_id, chain_type)
           DO UPDATE SET address = EXCLUDED.address,
                         normalized_address = EXCLUDED.normalized_address,
                         wallet_id = EXCLUDED.wallet_id,
                         updated_at = NOW()`,
          [
            user.user_id,
            wallet.chain_type,
            wallet.address,
            normalizeAddress(wallet.address),
            wallet.wallet_id || null,
          ]
        );
      }
    }

    const shardMap = new Map(
      Object.values(trackingStore.webhooks || {}).map((shard) => [shard.shard_key, shard])
    );

    for (const shard of shardMap.values()) {
      await client.query(
        `INSERT INTO tracking_shards
          (shard_key, webhook_id, chain_type, capacity, label, last_synced_at)
         VALUES ($1, $2, $3, $4, $5, $6)
         ON CONFLICT (shard_key)
         DO UPDATE SET webhook_id = EXCLUDED.webhook_id,
                       chain_type = EXCLUDED.chain_type,
                       capacity = EXCLUDED.capacity,
                       label = EXCLUDED.label,
                       last_synced_at = EXCLUDED.last_synced_at`,
        [
          shard.shard_key,
          shard.webhook_id,
          shard.chain_type,
          shard.capacity,
          shard.label || null,
          shard.last_synced_at || null,
        ]
      );
    }

    for (const trackedAddress of Object.values(trackingStore.addresses || {})) {
      if (!shardMap.has(trackedAddress.shard_key)) {
        skippedTrackedAddresses += 1;
        continue;
      }

      await client.query(
        `INSERT INTO tracked_addresses
          (normalized_address, shard_key, created_at, updated_at)
         VALUES ($1, $2, $3, $4)
         ON CONFLICT (normalized_address)
         DO UPDATE SET shard_key = EXCLUDED.shard_key,
                       updated_at = EXCLUDED.updated_at`,
        [
          trackedAddress.normalized_address,
          trackedAddress.shard_key,
          trackedAddress.created_at,
          trackedAddress.updated_at,
        ]
      );
    }

    for (const event of depositQueueStore.queue || []) {
      const { attempts, queued_at, updated_at, next_attempt_at, last_error, ...payload } = event;
      await client.query(
        `INSERT INTO deposit_queue_events
          (idempotency_key, payload, attempts, queued_at, updated_at, next_attempt_at, last_error)
         VALUES ($1, $2::jsonb, $3, $4, $5, $6, $7)
         ON CONFLICT (idempotency_key)
         DO UPDATE SET payload = EXCLUDED.payload,
                       attempts = EXCLUDED.attempts,
                       queued_at = EXCLUDED.queued_at,
                       updated_at = EXCLUDED.updated_at,
                       next_attempt_at = EXCLUDED.next_attempt_at,
                       last_error = EXCLUDED.last_error`,
        [
          payload.idempotency_key,
          JSON.stringify(payload),
          attempts,
          queued_at,
          updated_at,
          next_attempt_at,
          last_error || null,
        ]
      );
    }

    for (const [idempotencyKey, deliveredAt] of Object.entries(depositQueueStore.delivered || {})) {
      await client.query(
        `INSERT INTO deposit_delivered_events (idempotency_key, delivered_at)
         VALUES ($1, $2)
         ON CONFLICT (idempotency_key)
         DO UPDATE SET delivered_at = EXCLUDED.delivered_at`,
        [idempotencyKey, deliveredAt]
      );
    }

    for (const cursor of Object.values(reconciliationStore.cursors || {})) {
      await client.query(
        `INSERT INTO reconciliation_cursors (chain_type, from_block, updated_at)
         VALUES ($1, $2, $3)
         ON CONFLICT (chain_type)
         DO UPDATE SET from_block = EXCLUDED.from_block,
                       updated_at = EXCLUDED.updated_at`,
        [cursor.chain_type, cursor.from_block, cursor.updated_at]
      );
    }

    await client.query("COMMIT");
  } catch (error) {
    await client.query("ROLLBACK");
    throw error;
  } finally {
    client.release();
    await pool.end();
  }

  console.log("file state import complete", {
    users: Object.keys(mappingStore.users || {}).length,
    tracked_addresses: Object.keys(trackingStore.addresses || {}).length - skippedTrackedAddresses,
    skipped_tracked_addresses: skippedTrackedAddresses,
    tracking_shards: Object.keys(trackingStore.webhooks || {}).length,
    queued_events: (depositQueueStore.queue || []).length,
    delivered_events: Object.keys(depositQueueStore.delivered || {}).length,
    cursors: Object.keys(reconciliationStore.cursors || {}).length,
  });
}

main().catch((error) => {
  console.error("file state import failed", error instanceof Error ? error.message : String(error));
  process.exit(1);
});
