import jwt from 'jsonwebtoken';
import { CalendarRepository } from '../models/repositories/CalendarRepository';
import { BookingRepository } from '../models/repositories/BookingRepository';
import { UserRepository } from '../models/repositories/UserRepository';
import { BusyBlockRepository } from '../models/repositories/BusyBlockRepository';
import { BusyCacheService } from './BusyCacheService';
import { NotificationService } from './NotificationService';
import { getCalendarProvider } from './calendars/CalendarProviderFactory';
import { GoogleCalendarProvider } from './calendars/GoogleCalendarProvider';
import { MicrosoftCalendarProvider } from './calendars/MicrosoftCalendarProvider';
import { AppleCalendarProvider } from './calendars/AppleCalendarProvider';
import { encrypt } from '../utils/crypto';
import { generateUuid } from '../utils/uuid';
import { AppError } from '../utils/AppError';
import { CalendarConnection } from '../models/entities/CalendarConnection.entity';
import { oauthConfig } from '../config/oauth';
import { env } from '../config/env';
import { enqueueCalendarSync } from '../jobs/queues';

interface JwtPayload {
  sub: string;
  purpose?: string;
  provider?: string;
}

export class CalendarSyncService {
  constructor(
    private calendarRepo = new CalendarRepository(),
    private bookingRepo = new BookingRepository(),
    private userRepo = new UserRepository(),
    private busyBlockRepo = new BusyBlockRepository(),
    private busyCache = new BusyCacheService(),
    private notificationService = new NotificationService(),
  ) {}

  async listConnections(userId: string) {
    return this.calendarRepo.findConnectionsByUserId(userId);
  }

  getOAuthAuthorizeUrl(userId: string, provider: 'google' | 'microsoft'): string {
    const state = jwt.sign(
      { sub: userId, purpose: 'oauth', provider },
      env.JWT_SECRET,
      { expiresIn: '10m' },
    );

    if (provider === 'google') {
      if (!oauthConfig.google.clientId) {
        throw new AppError('validation_error', 400, 'Google OAuth not configured.');
      }
      return new GoogleCalendarProvider().getAuthUrl(state);
    }
    if (!oauthConfig.microsoft.clientId) {
      throw new AppError('validation_error', 400, 'Microsoft OAuth not configured.');
    }
    return new MicrosoftCalendarProvider().getAuthUrl(state);
  }

  async handleOAuthCallback(provider: 'google' | 'microsoft', code: string, state: string) {
    let userId: string;
    try {
      const payload = jwt.verify(state, env.JWT_SECRET) as JwtPayload;
      if (payload.purpose !== 'oauth' || payload.provider !== provider || !payload.sub) {
        throw new Error('Invalid OAuth state payload');
      }
      userId = payload.sub;
    } catch {
      throw new AppError('validation_error', 400, 'Invalid or expired OAuth state.');
    }

    let tokens: { accessToken: string; refreshToken?: string; expiresAt?: Date; email: string };
    let encrypted: { accessTokenEnc: string; refreshTokenEnc: string | null };

    if (provider === 'google') {
      const google = new GoogleCalendarProvider();
      tokens = await google.exchangeCode(code);
      encrypted = google.encryptTokens(tokens.accessToken, tokens.refreshToken);
    } else {
      const ms = new MicrosoftCalendarProvider();
      tokens = await ms.exchangeCode(code);
      encrypted = ms.encryptTokens(tokens.accessToken, tokens.refreshToken);
    }

    const connection = await this.calendarRepo.saveConnection({
      id: generateUuid(),
      userId,
      provider,
      providerAccountEmail: tokens.email,
      accessTokenEnc: encrypted.accessTokenEnc,
      refreshTokenEnc: encrypted.refreshTokenEnc,
      tokenExpiresAt: tokens.expiresAt ?? null,
      calendarConfig: [],
      status: 'connected',
    });

    try {
      const calProvider = getCalendarProvider(provider);
      const calendars = await calProvider.listCalendars(connection);
      if (calendars.length > 0) {
        connection.calendarConfig = calendars.map((c, i) => ({
          externalId: c.externalId,
          name: c.name,
          checkConflicts: true,
          writeTarget: i === 0,
        }));
      }
    } catch (err) {
      console.warn(
        `[CalendarSync] listCalendars failed for ${provider}:`,
        err instanceof Error ? err.message : err,
      );
    }

    // Ensure free/busy always has at least one calendar (primary / default)
    if (!connection.calendarConfig?.length) {
      connection.calendarConfig = [
        {
          externalId: provider === 'google' ? 'primary' : 'default',
          name: 'Primary',
          checkConflicts: true,
          writeTarget: true,
        },
      ];
    }
    await this.calendarRepo.updateConnection(connection.id, {
      calendarConfig: connection.calendarConfig,
    });

    // Populate busy times immediately (do not wait for 10-minute scheduler)
    await this.triggerBusySync(connection.id);

    return `${env.APP_URL}/settings?tab=calendars&connected=${provider}`;
  }

  async connectApple(userId: string, appleId: string, appPassword: string) {
    const apple = new AppleCalendarProvider();
    const encrypted = apple.storeCredentials(appleId, appPassword);
    const connection = await this.calendarRepo.saveConnection({
      id: generateUuid(),
      userId,
      provider: 'apple',
      providerAccountEmail: appleId,
      accessTokenEnc: encrypted.accessTokenEnc,
      refreshTokenEnc: null,
      calendarConfig: [{ externalId: 'default', name: 'iCloud', checkConflicts: true, writeTarget: true }],
      status: 'connected',
    });
    await this.triggerBusySync(connection.id);
    return connection;
  }

  async updateCalendarConfig(userId: string, connectionId: string, calendars: CalendarConnection['calendarConfig']) {
    const conn = await this.calendarRepo.findConnectionById(connectionId);
    if (!conn || conn.userId !== userId) {
      throw new AppError('not_found', 404, 'Connection not found.');
    }
    const writeTargets = calendars.filter((c) => c.writeTarget);
    if (writeTargets.length !== 1) {
      throw new AppError('validation_error', 400, 'Exactly one calendar must be the write target.');
    }
    const updated = await this.calendarRepo.updateConnection(connectionId, { calendarConfig: calendars });
    await this.triggerBusySync(connectionId);
    return updated;
  }

  async disconnect(userId: string, connectionId: string) {
    const conn = await this.calendarRepo.findConnectionById(connectionId);
    if (!conn || conn.userId !== userId) {
      throw new AppError('not_found', 404, 'Connection not found.');
    }
    try {
      const provider = getCalendarProvider(conn.provider);
      await provider.revokeToken?.(conn);
    } catch {
      // ignore revoke errors
    }
    await this.busyBlockRepo.deleteForConnection(connectionId);
    await this.busyCache.invalidateUser(userId);
    await this.calendarRepo.deleteConnection(connectionId);
  }

  async syncConnection(userId: string, connectionId: string) {
    const conn = await this.calendarRepo.findConnectionById(connectionId);
    if (!conn || conn.userId !== userId) {
      throw new AppError('not_found', 404, 'Connection not found.');
    }
    await this.triggerBusySync(connectionId);
    const updated = await this.calendarRepo.findConnectionById(connectionId);
    return updated ?? conn;
  }

  async listConnectionEvents(
    userId: string,
    connectionId: string,
    from: Date,
    to: Date,
    calendarId?: string,
  ) {
    if (to.getTime() - from.getTime() > 93 * 24 * 60 * 60 * 1000) {
      throw new AppError('validation_error', 400, 'Date range cannot exceed 93 days.');
    }
    const conn = await this.calendarRepo.findConnectionById(connectionId);
    if (!conn || conn.userId !== userId) {
      throw new AppError('not_found', 404, 'Connection not found.');
    }
    if (conn.status !== 'connected') {
      throw new AppError('validation_error', 400, 'Calendar connection is not active.');
    }

    const provider = getCalendarProvider(conn.provider);
    if (provider.listEvents) {
      const events = await provider.listEvents(conn, from, to, calendarId);
      const nameById = new Map(conn.calendarConfig.map((c) => [c.externalId, c.name]));
      return events.map((e) => ({
        ...e,
        calendarName: nameById.get(e.calendarId) ?? e.calendarId,
      }));
    }

    // Apple / providers without events API: show busy intervals as generic blocks
    const busy = await provider.getFreeBusy(conn, from, to);
    return busy.map((b, i) => ({
      id: `busy-${i}-${b.start.toISOString()}`,
      title: 'Busy',
      start: b.start,
      end: b.end,
      calendarId: calendarId ?? 'default',
      calendarName: conn.providerAccountEmail,
      allDay: false,
    }));
  }

  private async triggerBusySync(connectionId: string): Promise<void> {
    const queued = await enqueueCalendarSync('syncFreeBusy', { connectionId });
    if (!queued) {
      // Kafka unavailable — sync inline so booking slots still see busy times
      try {
        await this.syncFreeBusyForConnection(connectionId);
      } catch (err) {
        console.warn(
          `[CalendarSync] inline sync failed for ${connectionId}:`,
          err instanceof Error ? err.message : err,
        );
      }
    }
  }

  async pushCalendarEvent(bookingId: string) {
    const booking = await this.bookingRepo.findById(bookingId);
    if (!booking) return;

    const connection = await this.calendarRepo.getWriteTarget(booking.userId);
    if (!connection) return;

    const provider = getCalendarProvider(connection.provider);
    try {
      const { eventId, calendarId } = await provider.createEvent(connection, booking, booking.eventType);
      await this.calendarRepo.saveSync({
        id: generateUuid(),
        bookingId,
        connectionId: connection.id,
        externalCalendarId: calendarId,
        externalEventId: eventId,
        syncStatus: 'synced',
        syncedAt: new Date(),
      });
    } catch (err) {
      await this.calendarRepo.saveSync({
        id: generateUuid(),
        bookingId,
        connectionId: connection.id,
        externalCalendarId: 'unknown',
        externalEventId: 'unknown',
        syncStatus: 'failed',
        lastError: err instanceof Error ? err.message : 'Unknown error',
      });
    }
  }

  async deleteCalendarEvent(bookingId: string) {
    const syncs = await this.calendarRepo.findSyncByBooking(bookingId);
    for (const sync of syncs) {
      if (sync.syncStatus !== 'synced') continue;
      const connection = await this.calendarRepo.findConnectionById(sync.connectionId);
      if (!connection) continue;
      const provider = getCalendarProvider(connection.provider);
      try {
        await provider.deleteEvent(connection, sync.externalCalendarId, sync.externalEventId);
        sync.syncStatus = 'deleted';
      } catch {
        sync.syncStatus = 'failed';
      }
    }
  }

  async syncFreeBusyForConnection(connectionId: string) {
    const connection = await this.calendarRepo.findConnectionById(connectionId);
    if (!connection || connection.status !== 'connected') return;

    const from = new Date();
    const to = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
    const provider = getCalendarProvider(connection.provider);

    try {
      const busy = await provider.getFreeBusy(connection, from, to);

      // Persist to MySQL (source of truth) then warm Redis from all connections
      if (connection.provider !== 'apple' || busy.length > 0) {
        await this.busyBlockRepo.replaceForConnection(
          connection.userId,
          connection.id,
          connection.provider,
          busy,
        );
      }

      const allBusy = await this.busyBlockRepo.findInRange(connection.userId, from, to);
      const byDate = new Map<string, Array<{ start: Date; end: Date }>>();
      for (const block of allBusy) {
        const date = block.start.toISOString().slice(0, 10);
        if (!byDate.has(date)) byDate.set(date, []);
        byDate.get(date)!.push(block);
      }
      // Clear stale Redis days then rewrite from DB merge
      await this.busyCache.invalidateUser(connection.userId);
      for (const [date, blocks] of byDate) {
        await this.busyCache.setBusyBlocks(connection.userId, date, blocks);
      }

      await this.calendarRepo.updateConnection(connectionId, {
        lastSyncedAt: new Date(),
        status: 'connected',
      });
    } catch (err) {
      console.warn(
        `[CalendarSync] free/busy failed for ${connectionId}:`,
        err instanceof Error ? err.message : err,
      );
      await this.calendarRepo.updateConnection(connectionId, { status: 'error' });
      const user = await this.userRepo.findById(connection.userId);
      if (user) {
        await this.notificationService.sendSyncFailureEmail(
          user.email,
          connection.provider,
          user.id,
        );
      }
    }
  }

  async refreshExpiringTokens() {
    const connections = await this.calendarRepo.findExpiringTokens(10);
    for (const conn of connections) {
      if (conn.provider === 'apple') continue;
      try {
        const provider = getCalendarProvider(conn.provider);
        const tokens = await provider.refreshToken(conn);
        await this.calendarRepo.updateConnection(conn.id, {
          accessTokenEnc: encrypt(tokens.accessToken),
          refreshTokenEnc: tokens.refreshToken ? encrypt(tokens.refreshToken) : conn.refreshTokenEnc,
          tokenExpiresAt: tokens.expiresAt ?? null,
          status: 'connected',
        });
      } catch {
        await this.calendarRepo.updateConnection(conn.id, { status: 'expired' });
      }
    }
  }
}
