import { google } from 'googleapis';
import { oauthConfig } from '../../config/oauth';
import { decrypt, encrypt } from '../../utils/crypto';
import { CalendarProvider, ExternalCalendar, BusyInterval, TokenPair, ExternalCalendarEvent } from './CalendarProvider.interface';
import { CalendarConnection } from '../../models/entities/CalendarConnection.entity';
import { Booking } from '../../models/entities/Booking.entity';
import { EventType } from '../../models/entities/EventType.entity';

export class GoogleCalendarProvider implements CalendarProvider {
  private getOAuth2Client(connection?: CalendarConnection) {
    const oauth2 = new google.auth.OAuth2(
      oauthConfig.google.clientId,
      oauthConfig.google.clientSecret,
      oauthConfig.google.redirectUri,
    );
    if (connection?.accessTokenEnc) {
      oauth2.setCredentials({
        access_token: decrypt(connection.accessTokenEnc),
        ...(connection.refreshTokenEnc
          ? { refresh_token: decrypt(connection.refreshTokenEnc) }
          : {}),
      });
    }
    return oauth2;
  }

  getAuthUrl(state: string): string {
    const oauth2 = this.getOAuth2Client();
    return oauth2.generateAuthUrl({
      access_type: 'offline',
      prompt: 'consent',
      scope: oauthConfig.google.scopes,
      state,
    });
  }

  async exchangeCode(code: string): Promise<TokenPair & { email: string }> {
    const oauth2 = this.getOAuth2Client();
    const { tokens } = await oauth2.getToken(code);
    oauth2.setCredentials(tokens);
    const oauth2api = google.oauth2({ version: 'v2', auth: oauth2 });
    const userInfo = await oauth2api.userinfo.get();
    return {
      accessToken: tokens.access_token!,
      refreshToken: tokens.refresh_token ?? undefined,
      expiresAt: tokens.expiry_date ? new Date(tokens.expiry_date) : undefined,
      email: userInfo.data.email ?? '',
    };
  }

  async listCalendars(connection: CalendarConnection): Promise<ExternalCalendar[]> {
    const auth = this.getOAuth2Client(connection);
    const calendar = google.calendar({ version: 'v3', auth });
    const res = await calendar.calendarList.list();
    return (res.data.items ?? []).map((c) => ({
      externalId: c.id!,
      name: c.summary ?? c.id!,
    }));
  }

  async getFreeBusy(connection: CalendarConnection, from: Date, to: Date): Promise<BusyInterval[]> {
    const auth = this.getOAuth2Client(connection);
    const calendar = google.calendar({ version: 'v3', auth });
    let calendarIds = connection.calendarConfig
      .filter((c) => c.checkConflicts)
      .map((c) => ({ id: c.externalId }));

    // Always query at least the primary calendar so busy times populate after connect
    if (calendarIds.length === 0) {
      calendarIds = [{ id: 'primary' }];
    }

    const res = await calendar.freebusy.query({
      requestBody: {
        timeMin: from.toISOString(),
        timeMax: to.toISOString(),
        timeZone: 'UTC',
        items: calendarIds,
      },
    });

    const busy: BusyInterval[] = [];
    for (const cal of Object.values(res.data.calendars ?? {})) {
      for (const block of cal.busy ?? []) {
        if (block.start && block.end) {
          busy.push({ start: new Date(block.start), end: new Date(block.end) });
        }
      }
    }
    return busy;
  }

  async listEvents(
    connection: CalendarConnection,
    from: Date,
    to: Date,
    calendarId?: string,
  ): Promise<ExternalCalendarEvent[]> {
    const auth = this.getOAuth2Client(connection);
    const calendar = google.calendar({ version: 'v3', auth });

    let calendarIds: string[];
    if (calendarId) {
      calendarIds = [calendarId];
    } else {
      calendarIds = connection.calendarConfig
        .filter((c) => c.checkConflicts)
        .map((c) => c.externalId);
      if (calendarIds.length === 0) calendarIds = ['primary'];
    }

    const events: ExternalCalendarEvent[] = [];
    for (const id of calendarIds) {
      let pageToken: string | undefined;
      do {
        const res = await calendar.events.list({
          calendarId: id,
          timeMin: from.toISOString(),
          timeMax: to.toISOString(),
          singleEvents: true,
          orderBy: 'startTime',
          maxResults: 250,
          pageToken,
        });
        for (const item of res.data.items ?? []) {
          const startRaw = item.start?.dateTime ?? item.start?.date;
          const endRaw = item.end?.dateTime ?? item.end?.date;
          if (!startRaw || !endRaw || !item.id) continue;
          events.push({
            id: item.id,
            title: item.summary?.trim() || '(No title)',
            start: new Date(startRaw),
            end: new Date(endRaw),
            calendarId: id,
            allDay: Boolean(item.start?.date && !item.start?.dateTime),
          });
        }
        pageToken = res.data.nextPageToken ?? undefined;
      } while (pageToken);
    }
    return events.sort((a, b) => a.start.getTime() - b.start.getTime());
  }

  async createEvent(
    connection: CalendarConnection,
    booking: Booking,
    eventType: EventType,
  ): Promise<{ eventId: string; calendarId: string }> {
    const writeTarget = connection.calendarConfig.find((c) => c.writeTarget);
    const calendarId = writeTarget?.externalId ?? 'primary';
    const auth = this.getOAuth2Client(connection);
    const calendar = google.calendar({ version: 'v3', auth });

    const res = await calendar.events.insert({
      calendarId,
      requestBody: {
        summary: `${eventType.title} with ${booking.guestName}`,
        description: `Booked via calendar app\nGuest: ${booking.guestEmail}`,
        start: { dateTime: booking.startTime.toISOString(), timeZone: 'UTC' },
        end: { dateTime: booking.endTime.toISOString(), timeZone: 'UTC' },
        attendees: [{ email: booking.guestEmail, displayName: booking.guestName }],
      },
    });

    return { eventId: res.data.id!, calendarId };
  }

  async updateEvent(
    connection: CalendarConnection,
    calendarId: string,
    eventId: string,
    booking: Booking,
    eventType: EventType,
  ): Promise<void> {
    const auth = this.getOAuth2Client(connection);
    const calendar = google.calendar({ version: 'v3', auth });
    await calendar.events.patch({
      calendarId,
      eventId,
      requestBody: {
        start: { dateTime: booking.startTime.toISOString(), timeZone: 'UTC' },
        end: { dateTime: booking.endTime.toISOString(), timeZone: 'UTC' },
        summary: `${eventType.title} with ${booking.guestName}`,
      },
    });
  }

  async deleteEvent(connection: CalendarConnection, calendarId: string, eventId: string): Promise<void> {
    const auth = this.getOAuth2Client(connection);
    const calendar = google.calendar({ version: 'v3', auth });
    await calendar.events.delete({ calendarId, eventId });
  }

  async refreshToken(connection: CalendarConnection): Promise<TokenPair> {
    const oauth2 = this.getOAuth2Client(connection);
    const { credentials } = await oauth2.refreshAccessToken();
    return {
      accessToken: credentials.access_token!,
      refreshToken: credentials.refresh_token ?? undefined,
      expiresAt: credentials.expiry_date ? new Date(credentials.expiry_date) : undefined,
    };
  }

  async revokeToken(connection: CalendarConnection): Promise<void> {
    if (connection.accessTokenEnc) {
      const oauth2 = this.getOAuth2Client(connection);
      await oauth2.revokeToken(decrypt(connection.accessTokenEnc));
    }
  }

  encryptTokens(accessToken: string, refreshToken?: string) {
    return {
      accessTokenEnc: encrypt(accessToken),
      refreshTokenEnc: refreshToken ? encrypt(refreshToken) : null,
    };
  }
}
