/**
 * Google Calendar provider — calendar-module.
 *
 * Implements CalendarSyncProvider against Google Calendar API v3 using fetch
 * (no googleapis SDK — Workers-compatible).
 *
 * All HTTP errors throw with the response status and body for upstream handling.
 */
import type { CalendarSyncProvider, ExternalEvent, CalendarListItem, TokenRefreshResult } from '../provider'

const CALENDAR_API = 'https://www.googleapis.com/calendar/v3'
const TOKEN_URL = 'https://oauth2.googleapis.com/token'

export class GoogleCalendarProvider implements CalendarSyncProvider {
  readonly provider = 'google' as const

  constructor(
    private readonly clientId: string,
    private readonly clientSecret: string,
  ) {}

  // ── 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',
    })

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

    if (!res.ok) {
      const text = await res.text()
      throw new Error(`Google 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(`${CALENDAR_API}/users/me/calendarList`, {
      headers: { Authorization: `Bearer ${accessToken}` },
    })
    if (!res.ok) {
      throw new Error(`listCalendars failed (${res.status})`)
    }
    const data = await res.json() as { items: Array<{ id: string; summary: string; primary?: boolean }> }
    return (data.items ?? []).map((item) => ({
      id: item.id,
      name: item.summary,
      isPrimary: item.primary === true,
    }))
  }

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

  async pushEvent(
    accessToken: string,
    calendarId: string,
    ev: Omit<ExternalEvent, 'externalId' | 'externalCalendarId'>,
  ): Promise<{ externalId: string }> {
    const body = buildGoogleEventBody(ev)
    const res = await fetch(
      `${CALENDAR_API}/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 = buildGoogleEventBody(ev)
    const res = await fetch(
      `${CALENDAR_API}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(externalId)}`,
      {
        method: 'PUT',
        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(
      `${CALENDAR_API}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(externalId)}`,
      {
        method: 'DELETE',
        headers: { Authorization: `Bearer ${accessToken}` },
      },
    )
    // 410 = already gone; treat as success
    if (!res.ok && res.status !== 410) {
      throw new Error(`deleteEvent failed (${res.status})`)
    }
  }

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

  async fetchChanges(
    accessToken: string,
    calendarId: string,
    since: string | null,
  ): Promise<ExternalEvent[]> {
    const params = new URLSearchParams({
      singleEvents: 'true',
      maxResults: '250',
    })
    if (since) {
      params.set('updatedMin', since)
    }

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

    const data = await res.json() as {
      items: Array<{
        id: string
        summary?: string
        description?: string
        start: { dateTime?: string; date?: string }
        end: { dateTime?: string; date?: string }
        location?: string
        status?: string
      }>
    }

    return (data.items ?? [])
      .filter((item) => item.status !== 'cancelled')
      .map((item) => ({
        externalId: item.id,
        externalCalendarId: calendarId,
        title: item.summary ?? '(no title)',
        description: item.description ?? null,
        startAt: item.start.dateTime ?? `${item.start.date}T00:00:00Z`,
        endAt: item.end.dateTime ?? `${item.end.date}T00:00:00Z`,
        allDay: !item.start.dateTime,
        location: item.location ?? null,
      }))
  }

  // ── Register watch channel ──────────────────────────────────────────────────

  async registerWatch(
    accessToken: string,
    calendarId: string,
    callbackUrl: string,
    metadata?: Record<string, string>,
  ): Promise<{ channelId?: string; resourceId?: string }> {
    const channelId = metadata?.channelId ?? crypto.randomUUID()
    const body = {
      id: channelId,
      type: 'web_hook',
      address: callbackUrl,
      token: metadata?.token,
    }

    const res = await fetch(
      `${CALENDAR_API}/calendars/${encodeURIComponent(calendarId)}/events/watch`,
      {
        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; resourceId: string }
    return { channelId: data.id, resourceId: data.resourceId }
  }
}

// ── Internal helpers ──────────────────────────────────────────────────────────

function buildGoogleEventBody(ev: Omit<ExternalEvent, 'externalId' | 'externalCalendarId'>) {
  const startKey = ev.allDay ? 'date' : 'dateTime'
  const startValue = ev.allDay ? ev.startAt.slice(0, 10) : ev.startAt
  const endKey = ev.allDay ? 'date' : 'dateTime'
  const endValue = ev.allDay ? ev.endAt.slice(0, 10) : ev.endAt

  return {
    summary: ev.title,
    description: ev.description,
    location: ev.location,
    start: { [startKey]: startValue },
    end: { [endKey]: endValue },
  }
}

// ── OAuth URL builder (used by route handlers) ────────────────────────────────

export function buildGoogleOAuthUrl(
  clientId: string,
  redirectUri: string,
  state: string,
): string {
  const params = new URLSearchParams({
    client_id: clientId,
    redirect_uri: redirectUri,
    response_type: 'code',
    scope: [
      'https://www.googleapis.com/auth/calendar.readonly',
      'https://www.googleapis.com/auth/calendar.events',
      'openid',
      'profile',
    ].join(' '),
    access_type: 'offline',
    prompt: 'consent',
    state,
  })
  return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`
}

export async function exchangeGoogleCode(
  code: string,
  clientId: string,
  clientSecret: string,
  redirectUri: string,
): Promise<{
  accessToken: string
  refreshToken: string
  expiresAt: string
  sub: string
}> {
  const body = new URLSearchParams({
    code,
    client_id: clientId,
    client_secret: clientSecret,
    redirect_uri: redirectUri,
    grant_type: 'authorization_code',
  })

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

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

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

  // Decode id_token to get the sub (user identifier)
  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 { sub: string }

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