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';
import { oauthConfig } from '../../config/oauth';
import { encrypt, decrypt } from '../../utils/crypto';

export class MicrosoftCalendarProvider implements CalendarProvider {
  private tokenUrl = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
  private graphBase = 'https://graph.microsoft.com/v1.0';

  getAuthUrl(state: string): string {
    const params = new URLSearchParams({
      client_id: oauthConfig.microsoft.clientId,
      response_type: 'code',
      redirect_uri: oauthConfig.microsoft.redirectUri,
      scope: oauthConfig.microsoft.scopes.join(' '),
      state,
    });
    return `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?${params}`;
  }

  async exchangeCode(code: string): Promise<TokenPair & { email: string }> {
    const body = new URLSearchParams({
      client_id: oauthConfig.microsoft.clientId,
      client_secret: oauthConfig.microsoft.clientSecret,
      code,
      redirect_uri: oauthConfig.microsoft.redirectUri,
      grant_type: 'authorization_code',
    });
    const tokenRes = await fetch(this.tokenUrl, { method: 'POST', body });
    const tokens = (await tokenRes.json()) as {
      access_token: string;
      refresh_token?: string;
      expires_in?: number;
    };

    const meRes = await fetch(`${this.graphBase}/me`, {
      headers: { Authorization: `Bearer ${tokens.access_token}` },
    });
    const me = (await meRes.json()) as { mail?: string; userPrincipalName?: string };

    return {
      accessToken: tokens.access_token,
      refreshToken: tokens.refresh_token,
      expiresAt: tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1000) : undefined,
      email: me.mail ?? me.userPrincipalName ?? '',
    };
  }

  private getAccessToken(connection: CalendarConnection): string {
    if (!connection.accessTokenEnc) throw new Error('No access token');
    return decrypt(connection.accessTokenEnc);
  }

  async listCalendars(connection: CalendarConnection): Promise<ExternalCalendar[]> {
    const res = await fetch(`${this.graphBase}/me/calendars`, {
      headers: { Authorization: `Bearer ${this.getAccessToken(connection)}` },
    });
    const data = (await res.json()) as { value: Array<{ id: string; name: string }> };
    return (data.value ?? []).map((c) => ({ externalId: c.id, name: c.name }));
  }

  async getFreeBusy(connection: CalendarConnection, from: Date, to: Date): Promise<BusyInterval[]> {
    const email = connection.providerAccountEmail;
    const res = await fetch(`${this.graphBase}/me/calendar/getSchedule`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${this.getAccessToken(connection)}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        schedules: [email],
        startTime: { dateTime: from.toISOString(), timeZone: 'UTC' },
        endTime: { dateTime: to.toISOString(), timeZone: 'UTC' },
        availabilityViewInterval: 15,
      }),
    });
    const data = (await res.json()) as {
      value: Array<{ scheduleItems: Array<{ start: { dateTime: string }; end: { dateTime: string } }> }>;
    };
    const busy: BusyInterval[] = [];
    for (const item of data.value?.[0]?.scheduleItems ?? []) {
      busy.push({ start: new Date(item.start.dateTime), end: new Date(item.end.dateTime) });
    }
    return busy;
  }

  async listEvents(
    connection: CalendarConnection,
    from: Date,
    to: Date,
    calendarId?: string,
  ): Promise<ExternalCalendarEvent[]> {
    const token = this.getAccessToken(connection);
    const writeOrPrimary = calendarId
      || connection.calendarConfig.find((c) => c.writeTarget)?.externalId
      || connection.calendarConfig[0]?.externalId;

    const base = writeOrPrimary
      ? `${this.graphBase}/me/calendars/${encodeURIComponent(writeOrPrimary)}/calendarView`
      : `${this.graphBase}/me/calendarView`;
    const params = new URLSearchParams({
      startDateTime: from.toISOString(),
      endDateTime: to.toISOString(),
      $orderby: 'start/dateTime',
      $top: '250',
    });
    const res = await fetch(`${base}?${params}`, {
      headers: {
        Authorization: `Bearer ${token}`,
        Prefer: 'outlook.timezone="UTC"',
      },
    });
    if (!res.ok) {
      throw new Error(`Microsoft calendarView failed: ${res.status}`);
    }
    const data = (await res.json()) as {
      value?: Array<{
        id: string;
        subject?: string;
        isAllDay?: boolean;
        start: { dateTime: string };
        end: { dateTime: string };
      }>;
    };
    return (data.value ?? []).map((item) => ({
      id: item.id,
      title: item.subject?.trim() || '(No title)',
      start: new Date(item.start.dateTime.endsWith('Z') ? item.start.dateTime : `${item.start.dateTime}Z`),
      end: new Date(item.end.dateTime.endsWith('Z') ? item.end.dateTime : `${item.end.dateTime}Z`),
      calendarId: writeOrPrimary || 'default',
      allDay: Boolean(item.isAllDay),
    }));
  }

  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 ?? '';
    const url = calendarId
      ? `${this.graphBase}/me/calendars/${calendarId}/events`
      : `${this.graphBase}/me/events`;

    const res = await fetch(url, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${this.getAccessToken(connection)}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        subject: `${eventType.title} with ${booking.guestName}`,
        start: { dateTime: booking.startTime.toISOString(), timeZone: 'UTC' },
        end: { dateTime: booking.endTime.toISOString(), timeZone: 'UTC' },
        attendees: [{ emailAddress: { address: booking.guestEmail, name: booking.guestName }, type: 'required' }],
      }),
    });
    const event = (await res.json()) as { id: string };
    return { eventId: event.id, calendarId: calendarId || 'default' };
  }

  async updateEvent(
    connection: CalendarConnection,
    _calendarId: string,
    eventId: string,
    booking: Booking,
    eventType: EventType,
  ): Promise<void> {
    await fetch(`${this.graphBase}/me/events/${eventId}`, {
      method: 'PATCH',
      headers: {
        Authorization: `Bearer ${this.getAccessToken(connection)}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        subject: `${eventType.title} with ${booking.guestName}`,
        start: { dateTime: booking.startTime.toISOString(), timeZone: 'UTC' },
        end: { dateTime: booking.endTime.toISOString(), timeZone: 'UTC' },
      }),
    });
  }

  async deleteEvent(connection: CalendarConnection, _calendarId: string, eventId: string): Promise<void> {
    await fetch(`${this.graphBase}/me/events/${eventId}`, {
      method: 'DELETE',
      headers: { Authorization: `Bearer ${this.getAccessToken(connection)}` },
    });
  }

  async refreshToken(connection: CalendarConnection): Promise<TokenPair> {
    if (!connection.refreshTokenEnc) throw new Error('No refresh token');
    const body = new URLSearchParams({
      client_id: oauthConfig.microsoft.clientId,
      client_secret: oauthConfig.microsoft.clientSecret,
      refresh_token: decrypt(connection.refreshTokenEnc),
      grant_type: 'refresh_token',
    });
    const res = await fetch(this.tokenUrl, { method: 'POST', body });
    const tokens = (await res.json()) as { access_token: string; refresh_token?: string; expires_in?: number };
    return {
      accessToken: tokens.access_token,
      refreshToken: tokens.refresh_token,
      expiresAt: tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1000) : undefined,
    };
  }

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