/**
 * Referrals migration repository — dual-write pattern for partition key migration.
 *
 * Phase 1: Dual-write — writes go to both old (referrals) and new (referrals_v2) containers.
 *          Reads prefer new container, fallback to old.
 * Phase 2: Cutover — once backfill completes, switch to read-only from new container.
 * Phase 3: Cleanup — remove old container and dual-write logic.
 *
 * Migration flag: REFERRAL_MIGRATION_MODE env var
 *   - 'dual-write' (default): write to both, read from new with fallback
 *   - 'new-only': write/read only from referrals_v2
 *   - 'old-only': write/read only from referrals (pre-migration state)
 */

import { getCollection } from '../../lib/datastore.js';
import type { ReferralDoc } from './types.js';

export type MigrationMode = 'dual-write' | 'new-only' | 'old-only';

const DEFAULT_MIGRATION_MODE: MigrationMode =
  (process.env.REFERRAL_MIGRATION_MODE as MigrationMode) || 'dual-write';

function getMigrationMode(): MigrationMode {
  return DEFAULT_MIGRATION_MODE;
}

// Old container — original partition key /id
function oldCollection() {
  return getCollection<ReferralDoc>('referrals', '/id');
}

// New container — correct partition key /referrerId
function newCollection() {
  return getCollection<ReferralDoc>('referrals_v2', '/referrerId');
}

/**
 * Execute write operation based on migration mode.
 * Returns result from primary (new) collection.
 */
async function dualWrite<T>(
  operation: 'create' | 'update',
  fn: (coll: ReturnType<typeof newCollection>) => Promise<T>,
  oldFn?: (coll: ReturnType<typeof oldCollection>) => Promise<T>
): Promise<T> {
  const mode = getMigrationMode();

  // Always write to new container first (source of truth)
  const result = await fn(newCollection());

  // Best-effort write to old container for dual-write mode
  if (mode === 'dual-write' && oldFn) {
    try {
      await oldFn(oldCollection());
    } catch (err: unknown) {
      // Log but don't fail — new container is source of truth
      process.stderr.write(`[referrals-migration] Old container write failed: ${err}\n`);
    }
  }

  return result;
}

/**
 * Execute read operation with fallback pattern.
 * New container first, old container as fallback.
 */
async function readWithFallback<T>(
  fn: (coll: ReturnType<typeof newCollection>) => Promise<T | null>,
  oldFn: (coll: ReturnType<typeof oldCollection>) => Promise<T | null>
): Promise<T | null> {
  const mode = getMigrationMode();

  // Old-only mode: skip new container
  if (mode === 'old-only') {
    return oldFn(oldCollection());
  }

  // Try new container first
  const newResult = await fn(newCollection());
  if (newResult !== null) {
    return newResult;
  }

  // Fallback to old container (for data not yet backfilled)
  if (mode === 'dual-write') {
    return oldFn(oldCollection());
  }

  return null;
}

/**
 * Execute list/query operation on both containers and merge results.
 * Deduplicates by id.
 */
async function listWithMerge<T extends { id: string }>(
  fn: (coll: ReturnType<typeof newCollection>) => Promise<T[]>,
  oldFn: (coll: ReturnType<typeof oldCollection>) => Promise<T[]>
): Promise<T[]> {
  const mode = getMigrationMode();

  if (mode === 'old-only') {
    return oldFn(oldCollection());
  }

  if (mode === 'new-only') {
    return fn(newCollection());
  }

  // Dual-write: merge results from both containers
  const [newResults, oldResults] = await Promise.all([
    fn(newCollection()),
    oldFn(oldCollection()).catch((err) => {
      process.stderr.write(`[referrals-migration] Old container read failed: ${err}\n`);
      return [] as T[];
    }),
  ]);

  // Deduplicate by id (new container wins)
  const seen = new Set<string>();
  const merged: T[] = [];

  for (const doc of newResults) {
    if (!seen.has(doc.id)) {
      seen.add(doc.id);
      merged.push(doc);
    }
  }

  for (const doc of oldResults) {
    if (!seen.has(doc.id)) {
      seen.add(doc.id);
      merged.push(doc);
    }
  }

  return merged;
}

// ============================================================================
// Repository functions (mirrors repository.ts interface)
// ============================================================================

export async function listAll(
  limit = 100,
  offset = 0,
  productId?: string
): Promise<ReferralDoc[]> {
  const all = await listWithMerge(
    (coll) =>
      coll.findMany({
        filter: { productId: productId ?? '' },
        sort: { createdAt: -1 },
        limit: limit + offset,
      }),
    (coll) =>
      coll.findMany({
        filter: { productId: productId ?? '' },
        sort: { createdAt: -1 },
        limit: limit + offset,
      })
  );
  return all.slice(offset, offset + limit);
}

export async function getByReferrer(
  referrerId: string,
  productId: string
): Promise<ReferralDoc[]> {
  return listWithMerge(
    (coll) =>
      coll.findMany({
        filter: { productId, referrerId },
        sort: { createdAt: -1 },
      }),
    (coll) =>
      coll.findMany({
        filter: { productId, referrerId },
        sort: { createdAt: -1 },
      })
  );
}

export async function getByReferredEmail(
  email: string,
  productId: string
): Promise<ReferralDoc | null> {
  // For single-document lookups, try new first, then old
  const newResult = await newCollection().findOne({
    filter: { productId, referredEmail: email.toLowerCase() },
    sort: { createdAt: -1 },
  });

  if (newResult) return newResult;

  // Fallback to old container in dual-write mode
  if (getMigrationMode() === 'dual-write') {
    return oldCollection()
      .findOne({
        filter: { productId, referredEmail: email.toLowerCase() },
        sort: { createdAt: -1 },
      })
      .catch(() => null);
  }

  return null;
}

export async function getById(
  id: string,
  referrerId: string
): Promise<ReferralDoc | null> {
  return readWithFallback(
    async (coll) => {
      try {
        return await coll.findById(id, referrerId);
      } catch {
        return null;
      }
    },
    async (coll) => {
      try {
        // Old container uses id as partition key
        return await coll.findById(id, id);
      } catch {
        return null;
      }
    }
  );
}

export async function create(doc: ReferralDoc): Promise<ReferralDoc> {
  return dualWrite(
    'create',
    (coll) => coll.create(doc),
    (coll) => coll.create(doc)
  );
}

export async function update(
  id: string,
  referrerId: string,
  updates: Partial<ReferralDoc>
): Promise<ReferralDoc | null> {
  // Update new container (primary)
  const newResult = await newCollection()
    .update(id, referrerId, updates)
    .catch(() => null);

  // Best-effort update old container in dual-write mode
  if (getMigrationMode() === 'dual-write') {
    try {
      await oldCollection().update(id, id, updates);
    } catch (err: unknown) {
      process.stderr.write(`[referrals-migration] Old container update failed: ${err}\n`);
    }
  }

  return newResult;
}

export async function countReferrals(productId: string): Promise<{
  total: number;
  completed: number;
  rewarded: number;
}> {
  const all = await listWithMerge(
    (coll) =>
      coll.findMany({
        filter: { productId },
        sort: { createdAt: -1 },
        limit: 10000, // Reasonable limit for counts
      }),
    (coll) =>
      coll.findMany({
        filter: { productId },
        sort: { createdAt: -1 },
        limit: 10000,
      })
  );

  const completed = all.filter((r) =>
    ['signed_up', 'subscribed', 'rewarded'].includes(r.status)
  ).length;
  const rewarded = all.filter((r) => r.status === 'rewarded').length;

  return {
    total: all.length,
    completed,
    rewarded,
  };
}

// ============================================================================
// Migration management functions
// ============================================================================

export interface MigrationStatus {
  mode: MigrationMode;
  oldContainerCount: number;
  newContainerCount: number;
  pendingBackfillCount: number;
}

export async function getMigrationStatus(
  productId: string
): Promise<MigrationStatus> {
  const mode = getMigrationMode();

  const [oldDocs, newDocs] = await Promise.all([
    oldCollection()
      .findMany({ filter: { productId }, limit: 10000 })
      .catch(() => [] as ReferralDoc[]),
    newCollection()
      .findMany({ filter: { productId }, limit: 10000 })
      .catch(() => [] as ReferralDoc[]),
  ]);

  const newIds = new Set(newDocs.map((d) => d.id));
  const pendingBackfill = oldDocs.filter((d) => !newIds.has(d.id));

  return {
    mode,
    oldContainerCount: oldDocs.length,
    newContainerCount: newDocs.length,
    pendingBackfillCount: pendingBackfill.length,
  };
}

export interface BackfillResult {
  migrated: number;
  skipped: number;
  errors: string[];
}

/**
 * Backfill old container documents to new container.
 * Idempotent — safe to run multiple times.
 */
export async function backfillOldToNew(
  productId: string,
  batchSize = 100
): Promise<BackfillResult> {
  const result: BackfillResult = { migrated: 0, skipped: 0, errors: [] };

  // Get all docs from old container
  const oldDocs = await oldCollection()
    .findMany({ filter: { productId }, limit: 10000 })
    .catch(() => [] as ReferralDoc[]);

  if (oldDocs.length === 0) {
    return result;
  }

  // Check which already exist in new container
  const existingNew = await newCollection()
    .findMany({ filter: { productId }, limit: 10000 })
    .catch(() => [] as ReferralDoc[]);

  const existingIds = new Set(existingNew.map((d) => d.id));

  // Backfill in batches
  const toMigrate = oldDocs.filter((d) => !existingIds.has(d.id));

  for (let i = 0; i < toMigrate.length; i += batchSize) {
    const batch = toMigrate.slice(i, i + batchSize);

    await Promise.all(
      batch.map(async (doc) => {
        try {
          // Ensure referrerId exists (required for new partition key)
          if (!doc.referrerId) {
            result.errors.push(`Doc ${doc.id}: missing referrerId`);
            return;
          }

          await newCollection().create(doc);
          result.migrated++;
        } catch (err: unknown) {
          const error = err as { code?: number; message?: string };
          if (error.code === 409) {
            result.skipped++;
          } else {
            result.errors.push(`Doc ${doc.id}: ${error.message ?? String(err)}`);
          }
        }
      })
    );
  }

  return result;
}

/**
 * Verify consistency between old and new containers.
 * Returns list of inconsistencies (should be empty after successful backfill).
 */
export async function verifyConsistency(
  productId: string
): Promise<{ id: string; issue: string }[]> {
  const inconsistencies: { id: string; issue: string }[] = [];

  const [oldDocs, newDocs] = await Promise.all([
    oldCollection()
      .findMany({ filter: { productId }, limit: 10000 })
      .catch(() => [] as ReferralDoc[]),
    newCollection()
      .findMany({ filter: { productId }, limit: 10000 })
      .catch(() => [] as ReferralDoc[]),
  ]);

  const oldMap = new Map(oldDocs.map((d) => [d.id, d]));
  const newMap = new Map(newDocs.map((d) => [d.id, d]));

  // Check for docs in new but not in old (unexpected during migration)
  for (const [id, newDoc] of newMap) {
    if (!oldMap.has(id)) {
      // This is OK — new writes go to new container first
      continue;
    }

    const oldDoc = oldMap.get(id)!;

    // Compare key fields
    if (oldDoc.status !== newDoc.status) {
      inconsistencies.push({ id, issue: `status mismatch: ${oldDoc.status} vs ${newDoc.status}` });
    }
    if (oldDoc.referrerId !== newDoc.referrerId) {
      inconsistencies.push({ id, issue: `referrerId mismatch` });
    }
    if (oldDoc.referredEmail !== newDoc.referredEmail) {
      inconsistencies.push({ id, issue: `referredEmail mismatch` });
    }
  }

  // Check for docs in old but not in new (expected until backfill completes)
  for (const [id] of oldMap) {
    if (!newMap.has(id)) {
      inconsistencies.push({ id, issue: 'pending backfill (exists in old, missing in new)' });
    }
  }

  return inconsistencies;
}
