/**
 * Calendar Cross-Sync Engine
 *
 * Syncs events between calendar providers according to user-defined rules.
 * Supports one-way and two-way sync with fingerprint-based change detection
 * and anti-loop protection.
 */

import { PrismaClient } from '@prisma/client';
import { createHash } from 'crypto';
import { CalendarEvent, CalendarConnection, createCalendarEvent, updateCalendarEvent, deleteCalendarEvent } from './calendar-events';

const prisma = new PrismaClient();

// ---------------------------------------------------------------------------
// Fingerprinting
// ---------------------------------------------------------------------------

/**
 * Compute a stable fingerprint for a calendar event so we can detect changes.
 * Only title + start + end + description + location are included — we skip
 * provider-specific metadata that legitimately differs between copies.
 */
export function computeFingerprint(event: {
  title: string;
  startDateTime?: string | null;
  startDate?: string | null;
  endDateTime?: string | null;
  endDate?: string | null;
  description?: string | null;
  location?: string | null;
}): string {
  const raw = [
    (event.title || '').trim(),
    event.startDateTime || event.startDate || '',
    event.endDateTime || event.endDate || '',
    (event.description || '').trim(),
    (event.location || '').trim(),
  ].join('|');
  return createHash('sha1').update(raw).digest('hex');
}

// ---------------------------------------------------------------------------
// Anti-loop protection
// ---------------------------------------------------------------------------

/**
 * Returns true if this externalId was written by our sync engine
 * (i.e. it is a target event in some mapping). Prevents syncing events
 * that were themselves created by a sync rule, which would cause a loop.
 */
async function isAntiLoopTarget(
  userId: string,
  externalId: string,
  provider: string
): Promise<boolean> {
  const mapping = await prisma.calendarSyncMapping.findFirst({
    where: { userId, targetEventId: externalId, targetProvider: provider },
    select: { id: true },
  });
  return mapping !== null;
}

// ---------------------------------------------------------------------------
// Provider-connection helpers
// ---------------------------------------------------------------------------

/** Load a CalendarConnection from the DB and cast it to the shape calendar-events.ts expects */
async function loadConnection(connectionId: string): Promise<CalendarConnection | null> {
  const row = await prisma.calendarConnection.findUnique({
    where: { id: connectionId },
    select: {
      id: true,
      userId: true,
      provider: true,
      accessToken: true,
      refreshToken: true,
      expiresAt: true,
      calendars: true,
    },
  });
  if (!row) return null;
  return row as unknown as CalendarConnection;
}

// ---------------------------------------------------------------------------
// Event payload builder
// ---------------------------------------------------------------------------

/**
 * Build a CalendarEvent payload suitable for createCalendarEvent / updateCalendarEvent
 * from a cached event row + rule options.
 */
function buildPayload(
  cachedEvent: {
    title: string;
    description?: string | null;
    location?: string | null;
    startDateTime?: Date | null;
    startDate?: string | null;
    endDateTime?: Date | null;
    endDate?: string | null;
    isRecurring: boolean;
  },
  rule: {
    syncDescription: boolean;
    syncLocation: boolean;
    syncRecurring: boolean;
    titlePrefix: string;
    targetCalendarId: string;
  },
  targetProvider: string
): Partial<CalendarEvent> {
  const allDay = !cachedEvent.startDateTime;
  const start = allDay
    ? { date: cachedEvent.startDate || '' }
    : { dateTime: cachedEvent.startDateTime!.toISOString() };
  const end = allDay
    ? { date: cachedEvent.endDate || '' }
    : { dateTime: cachedEvent.endDateTime!.toISOString() };

  return {
    title: `${rule.titlePrefix}${cachedEvent.title}`,
    description: rule.syncDescription ? (cachedEvent.description ?? undefined) : undefined,
    location: rule.syncLocation ? (cachedEvent.location ?? undefined) : undefined,
    start,
    end,
    allDay,
    source: targetProvider as CalendarEvent['source'],
    calendarId: rule.targetCalendarId,
    calendarTitle: '',
  };
}

// ---------------------------------------------------------------------------
// Core sync logic for a single rule
// ---------------------------------------------------------------------------

async function syncOneDirection(params: {
  userId: string;
  rule: {
    id: string;
    sourceConnectionId: string;
    sourceCalendarId: string;
    targetConnectionId: string;
    targetCalendarId: string;
    syncDescription: boolean;
    syncLocation: boolean;
    syncRecurring: boolean;
    titlePrefix: string;
  };
  sourceConnection: CalendarConnection;
  targetConnection: CalendarConnection;
  weekStart: Date;
  weekEnd: Date;
}): Promise<{ created: number; updated: number; deleted: number; skipped: number }> {
  const { userId, rule, sourceConnection, targetConnection, weekStart, weekEnd } = params;
  let created = 0, updated = 0, deleted = 0, skipped = 0;

  // Fetch cached source events for the sync window
  const sourceEvents = await prisma.cachedCalendarEvent.findMany({
    where: {
      userId,
      connectionId: rule.sourceConnectionId,
      calendarId: rule.sourceCalendarId,
      OR: [
        { startDateTime: { gte: weekStart, lte: weekEnd } },
        { startDate: { gte: weekStart.toISOString().slice(0, 10), lte: weekEnd.toISOString().slice(0, 10) } },
      ],
    },
  });

  // Load existing mappings for this rule
  const existingMappings = await prisma.calendarSyncMapping.findMany({
    where: { ruleId: rule.id },
  });
  const mappingBySource = new Map(existingMappings.map(m => [m.sourceEventId, m]));

  // Track which sourceEventIds we processed (to detect deletions)
  const processedSourceIds = new Set<string>();

  for (const srcEvent of sourceEvents) {
    // Skip recurring events if rule says so
    if (srcEvent.isRecurring && !rule.syncRecurring) {
      skipped++;
      continue;
    }

    // Anti-loop: skip events that were themselves created by a sync rule
    if (await isAntiLoopTarget(userId, srcEvent.externalId, srcEvent.provider)) {
      skipped++;
      continue;
    }

    processedSourceIds.add(srcEvent.externalId);

    const currentFingerprint = computeFingerprint({
      title: srcEvent.title,
      startDateTime: srcEvent.startDateTime?.toISOString(),
      startDate: srcEvent.startDate,
      endDateTime: srcEvent.endDateTime?.toISOString(),
      endDate: srcEvent.endDate,
      description: srcEvent.description,
      location: srcEvent.location,
    });

    const existingMapping = mappingBySource.get(srcEvent.externalId);

    if (!existingMapping) {
      // New event — create on target
      try {
        const payload = buildPayload(srcEvent, rule, targetConnection.provider);
        const createdEvent = await createCalendarEvent(targetConnection, rule.targetCalendarId, payload);
        await prisma.calendarSyncMapping.create({
          data: {
            ruleId: rule.id,
            userId,
            sourceEventId: srcEvent.externalId,
            sourceProvider: srcEvent.provider,
            targetEventId: createdEvent.id,
            targetProvider: targetConnection.provider,
            targetCalendarId: rule.targetCalendarId,
            sourceFingerprint: currentFingerprint,
          },
        });
        created++;
      } catch (err) {
        console.error(`[cross-sync] Failed to create event ${srcEvent.externalId}:`, err);
        skipped++;
      }
    } else if (existingMapping.sourceFingerprint !== currentFingerprint) {
      // Changed event — update on target
      try {
        const payload = buildPayload(srcEvent, rule, targetConnection.provider);
        await updateCalendarEvent(targetConnection, rule.targetCalendarId, existingMapping.targetEventId, payload);
        await prisma.calendarSyncMapping.update({
          where: { id: existingMapping.id },
          data: { sourceFingerprint: currentFingerprint, lastSyncedAt: new Date() },
        });
        updated++;
      } catch (err) {
        console.error(`[cross-sync] Failed to update event ${srcEvent.externalId}:`, err);
        skipped++;
      }
    }
    // else: unchanged, skip
  }

  // Handle deletions: source events that have mappings but are no longer in cache
  for (const mapping of existingMappings) {
    if (!processedSourceIds.has(mapping.sourceEventId)) {
      // Source event no longer exists in the synced window → delete target
      try {
        await deleteCalendarEvent(targetConnection, rule.targetCalendarId, mapping.targetEventId);
        await prisma.calendarSyncMapping.delete({ where: { id: mapping.id } });
        deleted++;
      } catch (err) {
        console.error(`[cross-sync] Failed to delete target event ${mapping.targetEventId}:`, err);
        // Remove stale mapping anyway so it doesn't keep retrying
        await prisma.calendarSyncMapping.delete({ where: { id: mapping.id } }).catch(() => {});
      }
    }
  }

  return { created, updated, deleted, skipped };
}

// ---------------------------------------------------------------------------
// Public entry point
// ---------------------------------------------------------------------------

export interface SyncRuleResult {
  ruleId: string;
  ruleName: string;
  direction: string;
  created: number;
  updated: number;
  deleted: number;
  skipped: number;
  error?: string;
}

/**
 * Run all enabled cross-sync rules for a user.
 * Called after the regular calendar cache refresh so we always work
 * with fresh data from the cache.
 */
export async function runSyncRules(userId: string): Promise<SyncRuleResult[]> {
  const rules = await prisma.calendarSyncRule.findMany({
    where: { userId, enabled: true },
  });

  if (rules.length === 0) return [];

  // Define a rolling 8-week window (past 1 week + next 7 weeks)
  const now = new Date();
  const weekStart = new Date(now);
  weekStart.setDate(now.getDate() - 7);
  weekStart.setHours(0, 0, 0, 0);
  const weekEnd = new Date(now);
  weekEnd.setDate(now.getDate() + 7 * 7);
  weekEnd.setHours(23, 59, 59, 999);

  const results: SyncRuleResult[] = [];

  for (const rule of rules) {
    const result: SyncRuleResult = {
      ruleId: rule.id,
      ruleName: rule.name,
      direction: rule.direction,
      created: 0,
      updated: 0,
      deleted: 0,
      skipped: 0,
    };

    try {
      const sourceConn = await loadConnection(rule.sourceConnectionId);
      const targetConn = await loadConnection(rule.targetConnectionId);
      if (!sourceConn || !targetConn) {
        result.error = 'Connection not found';
        results.push(result);
        continue;
      }

      const ruleData = {
        id: rule.id,
        sourceConnectionId: rule.sourceConnectionId,
        sourceCalendarId: rule.sourceCalendarId,
        targetConnectionId: rule.targetConnectionId,
        targetCalendarId: rule.targetCalendarId,
        syncDescription: rule.syncDescription,
        syncLocation: rule.syncLocation,
        syncRecurring: rule.syncRecurring,
        titlePrefix: rule.titlePrefix,
      };

      // Source → Target (always for both one-way and two-way)
      const fwd = await syncOneDirection({
        userId,
        rule: ruleData,
        sourceConnection: sourceConn,
        targetConnection: targetConn,
        weekStart,
        weekEnd,
      });
      result.created += fwd.created;
      result.updated += fwd.updated;
      result.deleted += fwd.deleted;
      result.skipped += fwd.skipped;

      // Target → Source (only for two-way)
      if (rule.direction === 'two-way') {
        const reverseRuleData = {
          ...ruleData,
          id: rule.id + '_reverse', // virtual ID to keep mappings separate; we create real mappings per direction
          sourceConnectionId: rule.targetConnectionId,
          sourceCalendarId: rule.targetCalendarId,
          targetConnectionId: rule.sourceConnectionId,
          targetCalendarId: rule.sourceCalendarId,
        };

        // For two-way we use a special mapping key (rule.id + ':reverse')
        // We'll re-use the same rule.id but swap source/target in a clean way by
        // just calling syncOneDirection again — mappings are keyed by [ruleId, sourceEventId]
        // so 'source' for reverse is the original target calendar.
        // NOTE: to avoid collision we store reverse mappings under ruleId+'_R'
        const reverseMappingsExist = await prisma.calendarSyncMapping.findFirst({
          where: { ruleId: rule.id + '_R' },
          select: { id: true },
        });
        // We need a real ruleId to store under — we'll use a synthetic one-time id pattern
        // by creating a lightweight virtual rule inline. To keep things simple we just
        // pass the reverse as a separate call using a virtual rule id.
        const bwd = await syncOneDirection({
          userId,
          rule: { ...reverseRuleData, id: rule.id + '_R' },
          sourceConnection: targetConn,
          targetConnection: sourceConn,
          weekStart,
          weekEnd,
        });
        result.created += bwd.created;
        result.updated += bwd.updated;
        result.deleted += bwd.deleted;
        result.skipped += bwd.skipped;
      }

      // Update lastSyncedAt
      await prisma.calendarSyncRule.update({
        where: { id: rule.id },
        data: { lastSyncedAt: new Date() },
      });
    } catch (err: any) {
      result.error = err?.message || 'Unknown error';
    }

    results.push(result);
  }

  return results;
}
