/**
 * PG → Automerge Migration Tool
 *
 * One-time migration utility that reads data from PostgreSQL module schemas
 * and converts them into Automerge documents. Run per-module during the
 * local-first transition.
 *
 * Usage:
 *   import { migrateModule, notesMigration } from './migration/pg-to-automerge';
 *   await migrateModule(notesMigration, pool, 'demo', syncServer);
 *
 *   // Dry-run (no docs created):
 *   await migrateModule(notesMigration, pool, 'demo', syncServer, { dryRun: true });
 *
 *   // With disk backup:
 *   await migrateModule(notesMigration, pool, 'demo', syncServer, { backupDir: '/tmp/migration-backup' });
 *
 * Each module migration:
 *   1. Reads all rows from the PG schema
 *   2. Creates Automerge documents per the granularity rules
 *   3. Checks idempotency (skips already-migrated docs)
 *   4. Registers them with the SyncServer so they're available to clients
 *   5. Optionally writes the Automerge binaries to disk as backup
 */

import * as Automerge from '@automerge/automerge';
import { mkdirSync, writeFileSync } from 'node:fs';
import type { SyncServer } from '../sync-server';

// ============================================================================
// TYPES
// ============================================================================

export interface MigrationOptions {
  dryRun?: boolean;
  backupDir?: string;
}

export interface MigrationResult {
  module: string;
  docsCreated: number;
  docsSkipped: number;
  rowsMigrated: number;
  errors: string[];
  durationMs: number;
  dryRun: boolean;
}

/** Unified document metadata — present on all migrated docs. */
export interface DocMeta {
  module: string;
  collection: string;
  version: number;
  spaceSlug: string;
  createdAt: number;
}

/**
 * Module-specific migration adapter. Each module implements one of these
 * to define how its PG data maps to Automerge documents.
 */
export interface ModuleMigration<T = any> {
  module: string;
  collection: string;
  /**
   * Query PG and return rows grouped by target document.
   * Returns: Map<docIdSuffix, initialDocState>
   * An empty-string suffix produces a doc without a trailing segment.
   */
  extract(pool: any, space: string): Promise<Map<string, T>>;
}

// ============================================================================
// MIGRATION ENGINE
// ============================================================================

function buildMeta(migration: ModuleMigration, space: string): DocMeta {
  return {
    module: migration.module,
    collection: migration.collection,
    version: 1,
    spaceSlug: space,
    createdAt: Date.now(),
  };
}

/**
 * Run a module migration.
 */
export async function migrateModule(
  migration: ModuleMigration,
  pool: any,
  space: string,
  syncServer?: SyncServer,
  options: MigrationOptions = {}
): Promise<MigrationResult> {
  const start = Date.now();
  const { dryRun = false, backupDir } = options;
  const result: MigrationResult = {
    module: migration.module,
    docsCreated: 0,
    docsSkipped: 0,
    rowsMigrated: 0,
    errors: [],
    durationMs: 0,
    dryRun,
  };

  if (backupDir && !dryRun) {
    mkdirSync(backupDir, { recursive: true });
  }

  try {
    console.log(
      `[Migration] Starting ${migration.module} for space "${space}"` +
        (dryRun ? ' (DRY RUN)' : '') +
        '...'
    );

    const dataMap = await migration.extract(pool, space);
    const total = dataMap.size;
    let n = 0;

    for (const [suffix, initialState] of dataMap) {
      n++;
      try {
        const docId = suffix
          ? `${space}:${migration.module}:${migration.collection}:${suffix}`
          : `${space}:${migration.module}:${migration.collection}`;

        // Idempotency: skip if doc already exists
        if (syncServer?.getDoc(docId)) {
          console.log(
            `[Migration] ${migration.module}: ${n}/${total} — skipped "${docId}" (already exists)`
          );
          result.docsSkipped++;
          continue;
        }

        if (dryRun) {
          const rowCount = countItems(initialState);
          result.rowsMigrated += rowCount;
          result.docsCreated++;
          console.log(
            `[Migration] ${migration.module}: ${n}/${total} — would create "${docId}" (${rowCount} items)`
          );
          continue;
        }

        let doc = Automerge.init<any>();
        doc = Automerge.change(doc, `Migrate from PG: ${docId}`, (d: any) => {
          Object.assign(d, initialState);
        });

        // Register with sync server
        if (syncServer) {
          syncServer.setDoc(docId, doc);
        }

        // Disk backup
        if (backupDir) {
          const binary = Automerge.save(doc);
          const safeName = docId.replace(/[:/]/g, '_');
          writeFileSync(`${backupDir}/${safeName}.automerge`, binary);
        }

        result.docsCreated++;
        const rowCount = countItems(initialState);
        result.rowsMigrated += rowCount;

        console.log(
          `[Migration] ${migration.module}: ${n}/${total} — created "${docId}" (${rowCount} items)`
        );
      } catch (e) {
        const msg = `Failed to create doc for suffix "${suffix}": ${e}`;
        result.errors.push(msg);
        console.error(`[Migration] ${msg}`);
      }
    }
  } catch (e) {
    const msg = `Migration failed for ${migration.module}: ${e}`;
    result.errors.push(msg);
    console.error(`[Migration] ${msg}`);
  }

  result.durationMs = Date.now() - start;
  console.log(
    `[Migration] ${migration.module}: ${result.docsCreated} docs created, ` +
      `${result.docsSkipped} skipped, ` +
      `${result.rowsMigrated} rows, ${result.errors.length} errors, ` +
      `${result.durationMs}ms` +
      (dryRun ? ' (DRY RUN)' : '')
  );

  return result;
}

/**
 * Run multiple module migrations.
 */
export async function migrateAll(
  migrations: ModuleMigration[],
  pool: any,
  space: string,
  syncServer?: SyncServer,
  options: MigrationOptions = {}
): Promise<MigrationResult[]> {
  const results: MigrationResult[] = [];
  for (const migration of migrations) {
    const result = await migrateModule(migration, pool, space, syncServer, options);
    results.push(result);
  }
  return results;
}

// ============================================================================
// HELPER: safe row access (per-row error recovery)
// ============================================================================

function safeTimestamp(val: any): number {
  if (!val) return 0;
  return new Date(val).getTime();
}

function safePoint(val: any): { x: number; y: number } | null {
  if (!val) return null;
  // PG POINT comes as "(x,y)" string or {x, y} object
  if (typeof val === 'string') {
    const m = val.match(/\(([^,]+),([^)]+)\)/);
    if (m) return { x: parseFloat(m[1]), y: parseFloat(m[2]) };
  }
  if (typeof val === 'object' && 'x' in val) {
    return { x: Number(val.x), y: Number(val.y) };
  }
  return null;
}

// ============================================================================
// NOTES MIGRATION
// ============================================================================

/**
 * Notes: rnotes.notebooks + rnotes.notes + rnotes.note_tags/tags
 * Granularity: 1 doc per notebook ({space}:notes:notebooks:{notebookId})
 */
export const notesMigration: ModuleMigration = {
  module: 'notes',
  collection: 'notebooks',
  async extract(pool, space) {
    const result = new Map<string, any>();

    try {
      // Get all notebooks (no space_slug — notebooks are global, we migrate all)
      const { rows: notebooks } = await pool.query(
        `SELECT * FROM rnotes.notebooks ORDER BY created_at`
      );

      for (const nb of notebooks) {
        try {
          // Get notes for this notebook with tags
          const { rows: notes } = await pool.query(
            `SELECT n.*,
                    COALESCE(array_agg(t.name) FILTER (WHERE t.name IS NOT NULL), '{}') AS tags
             FROM rnotes.notes n
             LEFT JOIN rnotes.note_tags nt ON nt.note_id = n.id
             LEFT JOIN rnotes.tags t ON t.id = nt.tag_id
             WHERE n.notebook_id = $1
             GROUP BY n.id
             ORDER BY n.sort_order, n.created_at`,
            [nb.id]
          );

          const items: Record<string, any> = {};
          for (const row of notes) {
            try {
              items[row.id] = {
                id: row.id,
                notebookId: row.notebook_id,
                authorId: row.author_id || null,
                title: row.title || '',
                content: row.content || '',
                contentPlain: row.content_plain || '',
                type: row.type || 'NOTE',
                url: row.url || null,
                language: row.language || null,
                fileUrl: row.file_url || null,
                mimeType: row.mime_type || null,
                fileSize: row.file_size || null,
                duration: row.duration || null,
                isPinned: !!row.is_pinned,
                sortOrder: row.sort_order ?? 0,
                tags: row.tags || [],
                createdAt: safeTimestamp(row.created_at),
                updatedAt: safeTimestamp(row.updated_at),
              };
            } catch (e) {
              console.error(`[Migration] notes: error on note ${row.id}: ${e}`);
            }
          }

          result.set(nb.id, {
            meta: buildMeta(notesMigration, space),
            notebook: {
              id: nb.id,
              title: nb.title || '',
              slug: nb.slug || '',
              description: nb.description || '',
              coverColor: nb.cover_color || null,
              isPublic: !!nb.is_public,
              createdAt: safeTimestamp(nb.created_at),
              updatedAt: safeTimestamp(nb.updated_at),
            },
            items,
          });
        } catch (e) {
          console.error(`[Migration] notes: error on notebook ${nb.id}: ${e}`);
        }
      }
    } catch (e) {
      console.warn(`[Migration] notes: ${e}`);
    }

    return result;
  },
};

// ============================================================================
// WORK MIGRATION
// ============================================================================

/**
 * Tasks: rtasks.spaces + rtasks.tasks
 * Granularity: 1 doc per space/board ({space}:work:boards:{spaceId})
 */
export const workMigration: ModuleMigration = {
  module: 'work',
  collection: 'boards',
  async extract(pool, space) {
    const result = new Map<string, any>();

    try {
      const { rows: spaces } = await pool.query(
        `SELECT * FROM rtasks.spaces WHERE slug = $1`,
        [space]
      );

      for (const ws of spaces) {
        try {
          const { rows: tasks } = await pool.query(
            `SELECT * FROM rtasks.tasks WHERE space_id = $1 ORDER BY sort_order, created_at`,
            [ws.id]
          );

          const taskMap: Record<string, any> = {};
          for (const task of tasks) {
            try {
              taskMap[task.id] = {
                id: task.id,
                spaceId: task.space_id,
                title: task.title || '',
                description: task.description || '',
                status: task.status || 'todo',
                priority: task.priority || null,
                labels: task.labels || [],
                assigneeId: task.assignee_id || null,
                createdBy: task.created_by || null,
                sortOrder: task.sort_order ?? 0,
                createdAt: safeTimestamp(task.created_at),
                updatedAt: safeTimestamp(task.updated_at),
              };
            } catch (e) {
              console.error(`[Migration] work: error on task ${task.id}: ${e}`);
            }
          }

          result.set(ws.id, {
            meta: buildMeta(workMigration, space),
            board: {
              id: ws.id,
              name: ws.name || '',
              slug: ws.slug || '',
              description: ws.description || '',
              icon: ws.icon || null,
              ownerDid: ws.owner_did || null,
              statuses: ws.statuses || ['todo', 'in-progress', 'done'],
              labels: ws.labels || [],
              createdAt: safeTimestamp(ws.created_at),
              updatedAt: safeTimestamp(ws.updated_at),
            },
            tasks: taskMap,
          });
        } catch (e) {
          console.error(`[Migration] work: error on space ${ws.id}: ${e}`);
        }
      }
    } catch (e) {
      console.warn(`[Migration] work: ${e}`);
    }

    return result;
  },
};

// ============================================================================
// CALENDAR MIGRATION
// ============================================================================

/**
 * Cal: rcal.events + rcal.calendar_sources + rcal.locations
 * Granularity: 1 doc per space ({space}:cal:events)
 */
export const calMigration: ModuleMigration = {
  module: 'cal',
  collection: 'events',
  async extract(pool, space) {
    const result = new Map<string, any>();

    try {
      // Events have no space_slug — linked via source_id to calendar_sources.
      // We migrate all events and include source/location data inline.
      const { rows } = await pool.query(
        `SELECT e.*,
                cs.name AS source_name, cs.source_type, cs.color AS source_color,
                cs.url AS source_url, cs.is_active AS source_active,
                l.name AS loc_name, l.granularity AS loc_granularity,
                l.lat AS loc_lat, l.lng AS loc_lng
         FROM rcal.events e
         LEFT JOIN rcal.calendar_sources cs ON cs.id = e.source_id
         LEFT JOIN rcal.locations l ON l.id = e.location_id
         ORDER BY e.start_time`
      );

      if (rows.length === 0) return result;

      // Also get calendar sources as their own list
      const { rows: sources } = await pool.query(
        `SELECT * FROM rcal.calendar_sources ORDER BY created_at`
      );
      const sourcesMap: Record<string, any> = {};
      for (const s of sources) {
        sourcesMap[s.id] = {
          id: s.id,
          name: s.name || '',
          sourceType: s.source_type || '',
          url: s.url || null,
          color: s.color || null,
          isActive: s.is_active ?? true,
          isVisible: s.is_visible ?? true,
          syncIntervalMinutes: s.sync_interval_minutes || null,
          lastSyncedAt: safeTimestamp(s.last_synced_at),
          ownerId: s.owner_id || null,
          createdAt: safeTimestamp(s.created_at),
        };
      }

      const events: Record<string, any> = {};
      for (const row of rows) {
        try {
          events[row.id] = {
            id: row.id,
            title: row.title || '',
            description: row.description || '',
            startTime: safeTimestamp(row.start_time),
            endTime: safeTimestamp(row.end_time),
            allDay: !!row.all_day,
            timezone: row.timezone || null,
            rrule: row.rrule || null,
            status: row.status || null,
            visibility: row.visibility || null,
            sourceId: row.source_id || null,
            sourceName: row.source_name || null,
            sourceType: row.source_type || null,
            sourceColor: row.source_color || null,
            locationId: row.location_id || null,
            locationName: row.location_name || row.loc_name || null,
            coordinates: safePoint(row.coordinates),
            locationGranularity: row.location_granularity ?? row.loc_granularity ?? null,
            locationLat: row.loc_lat ?? null,
            locationLng: row.loc_lng ?? null,
            isVirtual: !!row.is_virtual,
            virtualUrl: row.virtual_url || null,
            virtualPlatform: row.virtual_platform || null,
            rToolSource: row.r_tool_source || null,
            rToolEntityId: row.r_tool_entity_id || null,
            attendees: row.attendees || [],
            attendeeCount: row.attendee_count ?? 0,
            metadata: row.metadata || null,
            createdAt: safeTimestamp(row.created_at),
            updatedAt: safeTimestamp(row.updated_at),
          };
        } catch (e) {
          console.error(`[Migration] cal: error on event ${row.id}: ${e}`);
        }
      }

      result.set('', {
        meta: buildMeta(calMigration, space),
        sources: sourcesMap,
        events,
      });
    } catch (e) {
      console.warn(`[Migration] cal: ${e}`);
    }

    return result;
  },
};

// ============================================================================
// VOTE MIGRATION
// ============================================================================

/**
 * Vote: rvote.proposals + rvote.votes + rvote.final_votes + rvote.spaces
 * Granularity: 1 doc per proposal ({space}:vote:proposals:{proposalId})
 */
export const voteMigration: ModuleMigration = {
  module: 'vote',
  collection: 'proposals',
  async extract(pool, space) {
    const result = new Map<string, any>();

    try {
      // Get space config
      let spaceConfig: any = null;
      try {
        const { rows } = await pool.query(
          `SELECT * FROM rvote.spaces WHERE slug = $1`,
          [space]
        );
        if (rows.length > 0) {
          const s = rows[0];
          spaceConfig = {
            slug: s.slug,
            name: s.name || '',
            description: s.description || '',
            ownerDid: s.owner_did || '',
            visibility: s.visibility || 'public',
            promotionThreshold: s.promotion_threshold ?? null,
            votingPeriodDays: s.voting_period_days ?? null,
            creditsPerDay: s.credits_per_day ?? null,
            maxCredits: s.max_credits ?? null,
            startingCredits: s.starting_credits ?? null,
            createdAt: safeTimestamp(s.created_at),
            updatedAt: safeTimestamp(s.updated_at),
          };
        }
      } catch (_) {
        // spaces table may not have data
      }

      const { rows: proposals } = await pool.query(
        `SELECT * FROM rvote.proposals WHERE space_slug = $1 ORDER BY created_at`,
        [space]
      );

      for (const prop of proposals) {
        try {
          // Conviction votes (quadratic)
          const { rows: votes } = await pool.query(
            `SELECT * FROM rvote.votes WHERE proposal_id = $1`,
            [prop.id]
          );

          const voteMap: Record<string, any> = {};
          for (const v of votes) {
            try {
              voteMap[v.id] = {
                id: v.id,
                userId: v.user_id,
                proposalId: v.proposal_id,
                weight: v.weight,
                creditCost: v.credit_cost,
                createdAt: safeTimestamp(v.created_at),
                decaysAt: safeTimestamp(v.decays_at),
              };
            } catch (e) {
              console.error(`[Migration] vote: error on vote ${v.id}: ${e}`);
            }
          }

          // Final votes (YES/NO/ABSTAIN)
          const { rows: finalVotes } = await pool.query(
            `SELECT * FROM rvote.final_votes WHERE proposal_id = $1`,
            [prop.id]
          );

          const finalVoteMap: Record<string, any> = {};
          for (const fv of finalVotes) {
            try {
              finalVoteMap[fv.id] = {
                id: fv.id,
                userId: fv.user_id,
                proposalId: fv.proposal_id,
                vote: fv.vote,
                createdAt: safeTimestamp(fv.created_at),
              };
            } catch (e) {
              console.error(`[Migration] vote: error on final_vote ${fv.id}: ${e}`);
            }
          }

          result.set(prop.id, {
            meta: buildMeta(voteMigration, space),
            spaceConfig,
            proposal: {
              id: prop.id,
              spaceSlug: prop.space_slug,
              authorId: prop.author_id,
              title: prop.title || '',
              description: prop.description || '',
              status: prop.status || 'RANKING',
              score: prop.score ?? 0,
              votingEndsAt: safeTimestamp(prop.voting_ends_at),
              finalYes: prop.final_yes ?? 0,
              finalNo: prop.final_no ?? 0,
              finalAbstain: prop.final_abstain ?? 0,
              createdAt: safeTimestamp(prop.created_at),
              updatedAt: safeTimestamp(prop.updated_at),
            },
            votes: voteMap,
            finalVotes: finalVoteMap,
          });
        } catch (e) {
          console.error(`[Migration] vote: error on proposal ${prop.id}: ${e}`);
        }
      }
    } catch (e) {
      console.warn(`[Migration] vote: ${e}`);
    }

    return result;
  },
};

// ============================================================================
// BOOKS MIGRATION
// ============================================================================

/**
 * Books: rbooks.books (global — no space_slug)
 * Granularity: 1 doc per space ({space}:books:catalog)
 * All books in one catalog doc; PDF paths as string refs
 */
export const booksMigration: ModuleMigration = {
  module: 'books',
  collection: 'catalog',
  async extract(pool, space) {
    const result = new Map<string, any>();

    try {
      const { rows } = await pool.query(
        `SELECT * FROM rbooks.books ORDER BY created_at`
      );

      if (rows.length === 0) return result;

      const items: Record<string, any> = {};
      for (const row of rows) {
        try {
          items[row.id] = {
            id: row.id,
            slug: row.slug || '',
            title: row.title || '',
            author: row.author || '',
            description: row.description || '',
            pdfPath: row.pdf_path || '',
            pdfSizeBytes: row.pdf_size_bytes ? Number(row.pdf_size_bytes) : 0,
            pageCount: row.page_count ?? 0,
            tags: row.tags || [],
            license: row.license || null,
            coverColor: row.cover_color || null,
            contributorId: row.contributor_id || null,
            contributorName: row.contributor_name || null,
            status: row.status || 'active',
            featured: !!row.featured,
            viewCount: row.view_count ?? 0,
            downloadCount: row.download_count ?? 0,
            createdAt: safeTimestamp(row.created_at),
            updatedAt: safeTimestamp(row.updated_at),
          };
        } catch (e) {
          console.error(`[Migration] books: error on book ${row.id}: ${e}`);
        }
      }

      result.set('', {
        meta: buildMeta(booksMigration, space),
        items,
      });
    } catch (e) {
      console.warn(`[Migration] books: ${e}`);
    }

    return result;
  },
};

// ============================================================================
// CART MIGRATION
// ============================================================================

/**
 * Cart: rcart.catalog_entries + rcart.orders
 * Granularity: 1 doc for catalog ({space}:cart:catalog),
 *              1 doc per order ({space}:cart:orders:{orderId})
 */
export const cartMigration: ModuleMigration = {
  module: 'cart',
  collection: 'catalog',
  async extract(pool, space) {
    const result = new Map<string, any>();

    try {
      // Catalog entries (global)
      const { rows: entries } = await pool.query(
        `SELECT * FROM rcart.catalog_entries ORDER BY created_at`
      );

      const catalogItems: Record<string, any> = {};
      for (const row of entries) {
        try {
          catalogItems[row.id] = {
            id: row.id,
            artifactId: row.artifact_id,
            artifact: row.artifact || {},
            title: row.title || '',
            productType: row.product_type || null,
            requiredCapabilities: row.required_capabilities || [],
            substrates: row.substrates || [],
            creatorId: row.creator_id || null,
            sourceSpace: row.source_space || null,
            tags: row.tags || [],
            status: row.status || 'active',
            createdAt: safeTimestamp(row.created_at),
            updatedAt: safeTimestamp(row.updated_at),
          };
        } catch (e) {
          console.error(`[Migration] cart: error on catalog entry ${row.id}: ${e}`);
        }
      }

      result.set('', {
        meta: {
          ...buildMeta(cartMigration, space),
          collection: 'catalog',
        },
        items: catalogItems,
      });

      // Orders — 1 doc per order
      const { rows: orders } = await pool.query(
        `SELECT * FROM rcart.orders ORDER BY created_at`
      );

      for (const row of orders) {
        try {
          const orderDoc = {
            meta: {
              ...buildMeta(cartMigration, space),
              collection: 'orders',
            },
            order: {
              id: row.id,
              catalogEntryId: row.catalog_entry_id,
              artifactId: row.artifact_id,
              buyerId: row.buyer_id || null,
              buyerLocation: row.buyer_location || null,
              buyerContact: row.buyer_contact || null,
              providerId: row.provider_id || null,
              providerName: row.provider_name || null,
              providerDistanceKm: row.provider_distance_km ?? null,
              quantity: row.quantity,
              productionCost: row.production_cost ? Number(row.production_cost) : null,
              creatorPayout: row.creator_payout ? Number(row.creator_payout) : null,
              communityPayout: row.community_payout ? Number(row.community_payout) : null,
              totalPrice: row.total_price ? Number(row.total_price) : null,
              currency: row.currency || 'USD',
              status: row.status || 'pending',
              paymentMethod: row.payment_method || null,
              paymentTx: row.payment_tx || null,
              paymentNetwork: row.payment_network || null,
              createdAt: safeTimestamp(row.created_at),
              paidAt: safeTimestamp(row.paid_at),
              acceptedAt: safeTimestamp(row.accepted_at),
              completedAt: safeTimestamp(row.completed_at),
              updatedAt: safeTimestamp(row.updated_at),
            },
          };

          // Orders use a different docId pattern: {space}:cart:orders:{orderId}
          // We return them with a key that the engine will use as suffix.
          // But since our collection is 'catalog', we need a custom key.
          // Prefix with 'orders:' so the engine builds the right docId.
          result.set(`orders:${row.id}`, orderDoc);
        } catch (e) {
          console.error(`[Migration] cart: error on order ${row.id}: ${e}`);
        }
      }
    } catch (e) {
      console.warn(`[Migration] cart: ${e}`);
    }

    return result;
  },
};

// ============================================================================
// PROVIDERS MIGRATION
// ============================================================================

/**
 * Providers: providers.providers (global, not space-scoped)
 * Granularity: 1 shared doc (global:providers:registry)
 */
export const providersMigration: ModuleMigration = {
  module: 'providers',
  collection: 'registry',
  async extract(pool, _space) {
    const result = new Map<string, any>();

    try {
      const { rows } = await pool.query(
        `SELECT * FROM providers.providers ORDER BY member_since, created_at`
      );

      if (rows.length === 0) return result;

      const items: Record<string, any> = {};
      for (const row of rows) {
        try {
          items[row.id] = {
            id: row.id,
            name: row.name || '',
            description: row.description || '',
            lat: row.lat,
            lng: row.lng,
            address: row.address || null,
            city: row.city || null,
            region: row.region || null,
            country: row.country ? row.country.trim() : null,
            serviceRadiusKm: row.service_radius_km ?? null,
            offersShipping: !!row.offers_shipping,
            capabilities: row.capabilities || [],
            substrates: row.substrates || [],
            standardDays: row.standard_days ?? null,
            rushDays: row.rush_days ?? null,
            rushSurchargePct: row.rush_surcharge_pct ?? null,
            pricing: row.pricing || {},
            communities: row.communities || [],
            contactEmail: row.contact_email || null,
            contactPhone: row.contact_phone || null,
            contactWebsite: row.contact_website || null,
            wallet: row.wallet || null,
            jobsCompleted: row.jobs_completed ?? 0,
            avgRating: row.avg_rating ?? null,
            memberSince: safeTimestamp(row.member_since),
            active: row.active ?? true,
            createdAt: safeTimestamp(row.created_at),
            updatedAt: safeTimestamp(row.updated_at),
          };
        } catch (e) {
          console.error(`[Migration] providers: error on provider ${row.id}: ${e}`);
        }
      }

      // Use 'global' as the space for providers (overrides the space parameter)
      result.set('', {
        meta: {
          module: 'providers',
          collection: 'registry',
          version: 1,
          spaceSlug: 'global',
          createdAt: Date.now(),
        },
        items,
      });
    } catch (e) {
      console.warn(`[Migration] providers: ${e}`);
    }

    return result;
  },
};

// ============================================================================
// FILES MIGRATION
// ============================================================================

/**
 * Files: rfiles.media_files + rfiles.memory_cards
 * Granularity: 1 doc per shared_space ({space}:files:cards)
 * Skips: access_logs (ephemeral), public_shares (ephemeral tokens)
 */
export const filesMigration: ModuleMigration = {
  module: 'files',
  collection: 'cards',
  async extract(pool, space) {
    const result = new Map<string, any>();

    try {
      // Get all unique shared_spaces
      const { rows: spacesResult } = await pool.query(
        `SELECT DISTINCT shared_space FROM rfiles.media_files WHERE shared_space IS NOT NULL
         UNION
         SELECT DISTINCT shared_space FROM rfiles.memory_cards`
      );

      for (const { shared_space: sharedSpace } of spacesResult) {
        try {
          // Media files for this shared space
          const { rows: files } = await pool.query(
            `SELECT * FROM rfiles.media_files WHERE shared_space = $1 ORDER BY created_at`,
            [sharedSpace]
          );

          const fileMap: Record<string, any> = {};
          for (const row of files) {
            try {
              fileMap[row.id] = {
                id: row.id,
                originalFilename: row.original_filename || '',
                title: row.title || null,
                description: row.description || '',
                mimeType: row.mime_type || null,
                fileSize: row.file_size ? Number(row.file_size) : 0,
                fileHash: row.file_hash || null,
                storagePath: row.storage_path || '',
                tags: row.tags || [],
                isProcessed: !!row.is_processed,
                processingError: row.processing_error || null,
                uploadedBy: row.uploaded_by || null,
                sharedSpace: row.shared_space || null,
                createdAt: safeTimestamp(row.created_at),
                updatedAt: safeTimestamp(row.updated_at),
              };
            } catch (e) {
              console.error(`[Migration] files: error on file ${row.id}: ${e}`);
            }
          }

          // Memory cards for this shared space
          const { rows: cards } = await pool.query(
            `SELECT * FROM rfiles.memory_cards WHERE shared_space = $1 ORDER BY position, created_at`,
            [sharedSpace]
          );

          const cardMap: Record<string, any> = {};
          for (const row of cards) {
            try {
              cardMap[row.id] = {
                id: row.id,
                sharedSpace: row.shared_space || '',
                title: row.title || '',
                body: row.body || '',
                cardType: row.card_type || null,
                tags: row.tags || [],
                position: row.position ?? 0,
                createdBy: row.created_by || null,
                createdAt: safeTimestamp(row.created_at),
                updatedAt: safeTimestamp(row.updated_at),
              };
            } catch (e) {
              console.error(`[Migration] files: error on card ${row.id}: ${e}`);
            }
          }

          // Use shared_space as suffix (not space param)
          result.set(sharedSpace, {
            meta: {
              ...buildMeta(filesMigration, space),
              sharedSpace,
            },
            files: fileMap,
            memoryCards: cardMap,
          });
        } catch (e) {
          console.error(`[Migration] files: error on shared_space ${sharedSpace}: ${e}`);
        }
      }
    } catch (e) {
      console.warn(`[Migration] files: ${e}`);
    }

    return result;
  },
};

// ============================================================================
// TRIPS MIGRATION
// ============================================================================

/**
 * Trips: rtrips.trips + destinations + itinerary_items + bookings + expenses + packing_items
 * Granularity: 1 doc per trip ({space}:trips:trips:{tripId})
 */
export const tripsMigration: ModuleMigration = {
  module: 'trips',
  collection: 'trips',
  async extract(pool, _space) {
    const result = new Map<string, any>();

    try {
      const { rows: trips } = await pool.query(
        `SELECT * FROM rtrips.trips ORDER BY created_at`
      );

      for (const trip of trips) {
        try {
          // Destinations
          const { rows: dests } = await pool.query(
            `SELECT * FROM rtrips.destinations WHERE trip_id = $1 ORDER BY sort_order, arrival_date`,
            [trip.id]
          );
          const destinations: Record<string, any> = {};
          for (const d of dests) {
            destinations[d.id] = {
              id: d.id,
              tripId: d.trip_id,
              name: d.name || '',
              country: d.country || null,
              lat: d.lat ?? null,
              lng: d.lng ?? null,
              arrivalDate: d.arrival_date?.toISOString?.() ?? d.arrival_date ?? null,
              departureDate: d.departure_date?.toISOString?.() ?? d.departure_date ?? null,
              notes: d.notes || '',
              sortOrder: d.sort_order ?? 0,
              createdAt: safeTimestamp(d.created_at),
            };
          }

          // Itinerary items
          const { rows: items } = await pool.query(
            `SELECT * FROM rtrips.itinerary_items WHERE trip_id = $1 ORDER BY date, sort_order`,
            [trip.id]
          );
          const itinerary: Record<string, any> = {};
          for (const it of items) {
            itinerary[it.id] = {
              id: it.id,
              tripId: it.trip_id,
              destinationId: it.destination_id || null,
              title: it.title || '',
              category: it.category || null,
              date: it.date?.toISOString?.() ?? it.date ?? null,
              startTime: it.start_time || null,
              endTime: it.end_time || null,
              notes: it.notes || '',
              sortOrder: it.sort_order ?? 0,
              createdAt: safeTimestamp(it.created_at),
            };
          }

          // Bookings
          const { rows: bkgs } = await pool.query(
            `SELECT * FROM rtrips.bookings WHERE trip_id = $1 ORDER BY start_date`,
            [trip.id]
          );
          const bookings: Record<string, any> = {};
          for (const b of bkgs) {
            bookings[b.id] = {
              id: b.id,
              tripId: b.trip_id,
              type: b.type || null,
              provider: b.provider || null,
              confirmationNumber: b.confirmation_number || null,
              cost: b.cost ? Number(b.cost) : null,
              currency: b.currency || null,
              startDate: b.start_date?.toISOString?.() ?? b.start_date ?? null,
              endDate: b.end_date?.toISOString?.() ?? b.end_date ?? null,
              status: b.status || null,
              notes: b.notes || '',
              createdAt: safeTimestamp(b.created_at),
            };
          }

          // Expenses
          const { rows: exps } = await pool.query(
            `SELECT * FROM rtrips.expenses WHERE trip_id = $1 ORDER BY date`,
            [trip.id]
          );
          const expenses: Record<string, any> = {};
          for (const ex of exps) {
            expenses[ex.id] = {
              id: ex.id,
              tripId: ex.trip_id,
              paidBy: ex.paid_by || null,
              description: ex.description || '',
              amount: ex.amount ? Number(ex.amount) : 0,
              currency: ex.currency || null,
              category: ex.category || null,
              date: ex.date?.toISOString?.() ?? ex.date ?? null,
              splitType: ex.split_type || null,
              createdAt: safeTimestamp(ex.created_at),
            };
          }

          // Packing items
          const { rows: packing } = await pool.query(
            `SELECT * FROM rtrips.packing_items WHERE trip_id = $1 ORDER BY sort_order`,
            [trip.id]
          );
          const packingItems: Record<string, any> = {};
          for (const p of packing) {
            packingItems[p.id] = {
              id: p.id,
              tripId: p.trip_id,
              addedBy: p.added_by || null,
              name: p.name || '',
              category: p.category || null,
              packed: !!p.packed,
              quantity: p.quantity ?? 1,
              sortOrder: p.sort_order ?? 0,
              createdAt: safeTimestamp(p.created_at),
            };
          }

          result.set(trip.id, {
            meta: buildMeta(tripsMigration, _space),
            trip: {
              id: trip.id,
              title: trip.title || '',
              slug: trip.slug || '',
              description: trip.description || '',
              startDate: trip.start_date?.toISOString?.() ?? trip.start_date ?? null,
              endDate: trip.end_date?.toISOString?.() ?? trip.end_date ?? null,
              budgetTotal: trip.budget_total ? Number(trip.budget_total) : null,
              budgetCurrency: trip.budget_currency || null,
              status: trip.status || 'planning',
              createdBy: trip.created_by || null,
              createdAt: safeTimestamp(trip.created_at),
              updatedAt: safeTimestamp(trip.updated_at),
            },
            destinations,
            itinerary,
            bookings,
            expenses,
            packingItems,
          });
        } catch (e) {
          console.error(`[Migration] trips: error on trip ${trip.id}: ${e}`);
        }
      }
    } catch (e) {
      console.warn(`[Migration] trips: ${e}`);
    }

    return result;
  },
};

// ============================================================================
// INBOX MIGRATION
// ============================================================================

/**
 * Inbox: rinbox.mailboxes + threads + comments + approvals + approval_signatures
 * Granularity: 1 doc per mailbox ({space}:inbox:mailboxes:{mailboxId})
 * Skips: sync_state (IMAP tracking, server-side only)
 */
export const inboxMigration: ModuleMigration = {
  module: 'inbox',
  collection: 'mailboxes',
  async extract(pool, _space) {
    const result = new Map<string, any>();

    try {
      const { rows: mailboxes } = await pool.query(
        `SELECT * FROM rinbox.mailboxes ORDER BY created_at`
      );

      for (const mb of mailboxes) {
        try {
          // Threads
          const { rows: threads } = await pool.query(
            `SELECT * FROM rinbox.threads WHERE mailbox_id = $1 ORDER BY received_at, created_at`,
            [mb.id]
          );

          const threadMap: Record<string, any> = {};
          for (const t of threads) {
            try {
              // Comments for this thread
              const { rows: comments } = await pool.query(
                `SELECT * FROM rinbox.comments WHERE thread_id = $1 ORDER BY created_at`,
                [t.id]
              );
              const commentList = comments.map((c: any) => ({
                id: c.id,
                threadId: c.thread_id,
                authorId: c.author_id,
                body: c.body || '',
                mentions: c.mentions || [],
                createdAt: safeTimestamp(c.created_at),
              }));

              threadMap[t.id] = {
                id: t.id,
                mailboxId: t.mailbox_id,
                messageId: t.message_id || null,
                subject: t.subject || '',
                fromAddress: t.from_address || null,
                fromName: t.from_name || null,
                toAddresses: t.to_addresses || [],
                ccAddresses: t.cc_addresses || [],
                bodyText: t.body_text || '',
                bodyHtml: t.body_html || '',
                tags: t.tags || [],
                status: t.status || 'open',
                isRead: !!t.is_read,
                isStarred: !!t.is_starred,
                assignedTo: t.assigned_to || null,
                hasAttachments: !!t.has_attachments,
                receivedAt: safeTimestamp(t.received_at),
                createdAt: safeTimestamp(t.created_at),
                comments: commentList,
              };
            } catch (e) {
              console.error(`[Migration] inbox: error on thread ${t.id}: ${e}`);
            }
          }

          // Approvals for this mailbox
          const { rows: approvals } = await pool.query(
            `SELECT * FROM rinbox.approvals WHERE mailbox_id = $1 ORDER BY created_at`,
            [mb.id]
          );

          const approvalMap: Record<string, any> = {};
          for (const a of approvals) {
            try {
              // Signatures for this approval
              const { rows: sigs } = await pool.query(
                `SELECT * FROM rinbox.approval_signatures WHERE approval_id = $1 ORDER BY signed_at`,
                [a.id]
              );
              const signatureList = sigs.map((s: any) => ({
                id: s.id,
                approvalId: s.approval_id,
                signerId: s.signer_id,
                vote: s.vote || '',
                signedAt: safeTimestamp(s.signed_at),
              }));

              approvalMap[a.id] = {
                id: a.id,
                mailboxId: a.mailbox_id,
                threadId: a.thread_id || null,
                authorId: a.author_id,
                subject: a.subject || '',
                bodyText: a.body_text || '',
                bodyHtml: a.body_html || '',
                toAddresses: a.to_addresses || [],
                ccAddresses: a.cc_addresses || [],
                status: a.status || 'pending',
                requiredSignatures: a.required_signatures ?? 1,
                safeTxHash: a.safe_tx_hash || null,
                createdAt: safeTimestamp(a.created_at),
                resolvedAt: safeTimestamp(a.resolved_at),
                signatures: signatureList,
              };
            } catch (e) {
              console.error(`[Migration] inbox: error on approval ${a.id}: ${e}`);
            }
          }

          // Members
          const { rows: members } = await pool.query(
            `SELECT * FROM rinbox.mailbox_members WHERE mailbox_id = $1`,
            [mb.id]
          );
          const memberList = members.map((m: any) => ({
            id: m.id,
            mailboxId: m.mailbox_id,
            userId: m.user_id,
            role: m.role || 'member',
            joinedAt: safeTimestamp(m.joined_at),
          }));

          result.set(mb.id, {
            meta: buildMeta(inboxMigration, _space),
            mailbox: {
              id: mb.id,
              workspaceId: mb.workspace_id || null,
              slug: mb.slug || '',
              name: mb.name || '',
              email: mb.email || '',
              description: mb.description || '',
              visibility: mb.visibility || 'private',
              ownerDid: mb.owner_did || '',
              safeAddress: mb.safe_address || null,
              safeChainId: mb.safe_chain_id ?? null,
              approvalThreshold: mb.approval_threshold ?? 1,
              createdAt: safeTimestamp(mb.created_at),
            },
            members: memberList,
            threads: threadMap,
            approvals: approvalMap,
          });
        } catch (e) {
          console.error(`[Migration] inbox: error on mailbox ${mb.id}: ${e}`);
        }
      }
    } catch (e) {
      console.warn(`[Migration] inbox: ${e}`);
    }

    return result;
  },
};

// ============================================================================
// SPLAT MIGRATION
// ============================================================================

/**
 * Splat: rsplat.splats + rsplat.source_files
 * Granularity: 1 doc per space ({space}:splat:scenes)
 */
export const splatMigration: ModuleMigration = {
  module: 'splat',
  collection: 'scenes',
  async extract(pool, space) {
    const result = new Map<string, any>();

    try {
      const { rows: splats } = await pool.query(
        `SELECT * FROM rsplat.splats WHERE space_slug = $1 ORDER BY created_at`,
        [space]
      );

      if (splats.length === 0) return result;

      const items: Record<string, any> = {};
      for (const row of splats) {
        try {
          // Source files for this splat
          const { rows: sourceFiles } = await pool.query(
            `SELECT * FROM rsplat.source_files WHERE splat_id = $1 ORDER BY created_at`,
            [row.id]
          );

          const sources = sourceFiles.map((sf: any) => ({
            id: sf.id,
            splatId: sf.splat_id,
            filePath: sf.file_path || '',
            fileName: sf.file_name || '',
            mimeType: sf.mime_type || null,
            fileSizeBytes: sf.file_size_bytes ? Number(sf.file_size_bytes) : 0,
            createdAt: safeTimestamp(sf.created_at),
          }));

          items[row.id] = {
            id: row.id,
            slug: row.slug || '',
            title: row.title || '',
            description: row.description || '',
            filePath: row.file_path || '',
            fileFormat: row.file_format || '',
            fileSizeBytes: row.file_size_bytes ? Number(row.file_size_bytes) : 0,
            tags: row.tags || [],
            spaceSlug: row.space_slug || '',
            contributorId: row.contributor_id || null,
            contributorName: row.contributor_name || null,
            source: row.source || null,
            status: row.status || 'active',
            viewCount: row.view_count ?? 0,
            paymentTx: row.payment_tx || null,
            paymentNetwork: row.payment_network || null,
            createdAt: safeTimestamp(row.created_at),
            processingStatus: row.processing_status || null,
            processingError: row.processing_error || null,
            sourceFileCount: row.source_file_count ?? 0,
            sourceFiles: sources,
          };
        } catch (e) {
          console.error(`[Migration] splat: error on splat ${row.id}: ${e}`);
        }
      }

      result.set('', {
        meta: buildMeta(splatMigration, space),
        items,
      });
    } catch (e) {
      console.warn(`[Migration] splat: ${e}`);
    }

    return result;
  },
};

// ============================================================================
// ALL MIGRATIONS (convenience export)
// ============================================================================

export const allMigrations: ModuleMigration[] = [
  notesMigration,
  workMigration,
  calMigration,
  voteMigration,
  booksMigration,
  cartMigration,
  providersMigration,
  filesMigration,
  tripsMigration,
  inboxMigration,
  splatMigration,
];

// ============================================================================
// HELPERS
// ============================================================================

function countItems(state: any): number {
  if (!state || typeof state !== 'object') return 1;

  let count = 0;
  for (const [key, value] of Object.entries(state)) {
    if (key === 'meta') continue;
    if (value && typeof value === 'object' && !Array.isArray(value)) {
      count += Object.keys(value).length;
    }
  }
  return Math.max(count, 1);
}
