/**
 * Outlook (Microsoft Graph) calendar provider — calendar-module.
 *
 * Implements CalendarSyncProvider against Microsoft Graph API.
 * Uses OAuth 2.0 authorization code flow with scope Calendars.ReadWrite offline_access.
 * Workers-compatible: uses fetch only, no MSAL SDK.
 */
import type { CalendarSyncProvider, ExternalEvent, CalendarListItem, TokenRefreshResult } from '../provider'

const GRAPH_API = 'https://graph.microsoft.com/v1.0'
const TOKEN_URL_BASE = 'https://login.microsoftonline.com'

export class OutlookCalendarProvider implements CalendarSyncProvider {
  readonly provider = 'outlook' as const

  constructor(
    private readonly clientId: string,
    private readonly clientSecret: string,
    private readonly tenantMsId: string = 'common',
  ) {}

  private get tokenUrl(): string {
    return `${TOKEN_URL_BASE}/${this.tenantMsId}/oauth2/v2.0/token`
  }

  // ── Token refresh ───────────────────────────────────────────────────────────

  async refreshAccessToken(refreshToken: string): Promise<TokenRefreshResult> {
    const body = new URLSearchParams({
      client_id: this.clientId,
      client_secret: this.clientSecret,
      refresh_token: refreshToken,
      grant_type: 'refresh_token',
      scope: 'Calendars.ReadWrite offline_access',
    })

    const res = await fetch(this.tokenUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: body.toString(),
    })

    if (!res.ok) {
      const text = await res.text()
      throw new Error(`Outlook token refresh failed (${res.status}): ${text}`)
    }

    const data = await res.json() as { access_token: string; expires_in: number }
    const expiresAt = new Date(Date.now() + data.expires_in * 1000).toISOString()
    return { accessToken: data.access_token, expiresAt }
  }

  // ── Calendar list ───────────────────────────────────────────────────────────

  async listCalendars(accessToken: string): Promise<CalendarListItem[]> {
    const res = await fetch(`${GRAPH_API}/me/calendars`, {
      headers: { Authorization: `Bearer ${accessToken}` },
    })
    if (!res.ok) {
      throw new Error(`listCalendars failed (${res.status})`)
    }
    const data = await res.json() as { value: Array<{ id: string; name: string; isDefaultCalendar?: boolean }> }
    return (data.value ?? []).map((c) => ({
      id: c.id,
      name: c.name,
      isPrimary: c.isDefaultCalendar === true,
    }))
  }

  // ── Push event ──────────────────────────────────────────────────────────────

  async pushEvent(
    accessToken: string,
    calendarId: string,
    ev: Omit<ExternalEvent, 'externalId' | 'externalCalendarId'>,
  ): Promise<{ externalId: string }> {
    const body = buildGraphEventBody(ev)
    const res = await fetch(
      `${GRAPH_API}/me/calendars/${encodeURIComponent(calendarId)}/events`,
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${accessToken}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(body),
      },
    )
    if (!res.ok) {
      throw new Error(`pushEvent failed (${res.status})`)
    }
    const data = await res.json() as { id: string }
    return { externalId: data.id }
  }

  // ── Update event ────────────────────────────────────────────────────────────

  async updateEvent(
    accessToken: string,
    calendarId: string,
    externalId: string,
    ev: Omit<ExternalEvent, 'externalId' | 'externalCalendarId'>,
  ): Promise<void> {
    const body = buildGraphEventBody(ev)
    const res = await fetch(
      `${GRAPH_API}/me/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(externalId)}`,
      {
        method: 'PATCH',
        headers: {
          Authorization: `Bearer ${accessToken}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(body),
      },
    )
    if (!res.ok) {
      throw new Error(`updateEvent failed (${res.status})`)
    }
  }

  // ── Delete event ────────────────────────────────────────────────────────────

  async deleteEvent(
    accessToken: string,
    calendarId: string,
    externalId: string,
  ): Promise<void> {
    const res = await fetch(
      `${GRAPH_API}/me/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(externalId)}`,
      {
        method: 'DELETE',
        headers: { Authorization: `Bearer ${accessToken}` },
      },
    )
    if (!res.ok && res.status !== 404) {
      throw new Error(`deleteEvent failed (${res.status})`)
    }
  }

  // ── Fetch changes ───────────────────────────────────────────────────────────

  async fetchChanges(
    accessToken: string,
    calendarId: string,
    since: string | null,
  ): Promise<ExternalEvent[]> {
    const params = new URLSearchParams({ '$top': '250' })
    if (since) {
      params.set('$filter', `lastModifiedDateTime ge ${since}`)
    }

    const url = `${GRAPH_API}/me/calendars/${encodeURIComponent(calendarId)}/events?${params.toString()}`
    const res = await fetch(url, {
      headers: {
        Authorization: `Bearer ${accessToken}`,
        Prefer: 'outlook.timezone="UTC"',
      },
    })
    if (!res.ok) {
      throw new Error(`fetchChanges failed (${res.status})`)
    }

    const data = await res.json() as {
      value: Array<{
        id: string
        subject?: string
        body?: { content?: string }
        start: { dateTime: string; timeZone: string }
        end: { dateTime: string; timeZone: string }
        location?: { displayName?: string }
        isAllDay?: boolean
        isCancelled?: boolean
      }>
    }

    return (data.value ?? [])
      .filter((item) => !item.isCancelled)
      .map((item) => ({
        externalId: item.id,
        externalCalendarId: calendarId,
        title: item.subject ?? '(no title)',
        description: item.body?.content ?? null,
        startAt: ensureUtc(item.start.dateTime),
        endAt: ensureUtc(item.end.dateTime),
        allDay: item.isAllDay === true,
        location: item.location?.displayName ?? null,
      }))
  }

  // ── Register subscription (push notifications) ──────────────────────────────

  async registerWatch(
    accessToken: string,
    _calendarId: string,
    callbackUrl: string,
    metadata?: Record<string, string>,
  ): Promise<{ channelId?: string; resourceId?: string }> {
    const expiresAt = new Date(Date.now() + 3 * 24 * 3600 * 1000).toISOString()
    const body = {
      changeType: 'created,updated,deleted',
      notificationUrl: callbackUrl,
      resource: '/me/events',
      expirationDateTime: expiresAt,
      clientState: metadata?.clientState ?? crypto.randomUUID(),
    }

    const res = await fetch(`${GRAPH_API}/subscriptions`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    })
    if (!res.ok) {
      throw new Error(`registerWatch failed (${res.status})`)
    }
    const data = await res.json() as { id: string }
    return { channelId: data.id }
  }
}

// ── Helpers ───────────────────────────────────────────────────────────────────

function buildGraphEventBody(ev: Omit<ExternalEvent, 'externalId' | 'externalCalendarId'>) {
  return {
    subject: ev.title,
    body: ev.description ? { contentType: 'text', content: ev.description } : undefined,
    location: ev.location ? { displayName: ev.location } : undefined,
    start: { dateTime: ev.startAt, timeZone: 'UTC' },
    end: { dateTime: ev.endAt, timeZone: 'UTC' },
    isAllDay: ev.allDay,
  }
}

function ensureUtc(dt: string): string {
  if (dt.endsWith('Z') || dt.includes('+')) return dt
  return `${dt}Z`
}

// ── OAuth URL builder + code exchange ─────────────────────────────────────────

export function buildOutlookOAuthUrl(
  clientId: string,
  redirectUri: string,
  state: string,
  tenantMsId = 'common',
): string {
  const params = new URLSearchParams({
    client_id: clientId,
    redirect_uri: redirectUri,
    response_type: 'code',
    scope: 'Calendars.ReadWrite offline_access openid profile',
    state,
    response_mode: 'query',
  })
  return `${TOKEN_URL_BASE}/${tenantMsId}/oauth2/v2.0/authorize?${params.toString()}`
}

export async function exchangeOutlookCode(
  code: string,
  clientId: string,
  clientSecret: string,
  redirectUri: string,
  tenantMsId = 'common',
): Promise<{
  accessToken: string
  refreshToken: string
  expiresAt: string
  oid: string
}> {
  const body = new URLSearchParams({
    client_id: clientId,
    client_secret: clientSecret,
    code,
    redirect_uri: redirectUri,
    grant_type: 'authorization_code',
    scope: 'Calendars.ReadWrite offline_access openid profile',
  })

  const res = await fetch(`${TOKEN_URL_BASE}/${tenantMsId}/oauth2/v2.0/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: body.toString(),
  })

  if (!res.ok) {
    const text = await res.text()
    throw new Error(`Outlook code exchange failed (${res.status}): ${text}`)
  }

  const data = await res.json() as {
    access_token: string
    refresh_token: string
    expires_in: number
    id_token: string
  }

  const payload = data.id_token.split('.')[1]
  if (!payload) {
    throw new Error('Invalid id_token: missing payload segment')
  }
  const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/'))) as { oid: string }

  return {
    accessToken: data.access_token,
    refreshToken: data.refresh_token,
    expiresAt: new Date(Date.now() + data.expires_in * 1000).toISOString(),
    oid: decoded.oid,
  }
}
