import type { Database } from '@pig/db';
import { Hono } from 'hono';
import { z } from 'zod';
import { requireAnyTeamCapability } from '../lib/auth';
import type { ApiEnv } from '../lib/mutation';
import { MutationError } from '../lib/mutation';
import {
  GoogleApiError,
  GoogleSheetsService,
  type GoogleSheetsConfig,
} from '../services/google-sheets';

const identifier = z.string().trim().min(1).max(255).regex(/^[A-Za-z0-9_-]+$/);
const listSchema = z.object({
  pageToken: z.string().min(1).max(2_048).optional(),
  search: z.string().trim().max(100).optional(),
});
const tableSchema = z.object({
  spreadsheetId: identifier,
  sheetId: z.number().int().nonnegative(),
  range: z.string().trim().min(1).max(40),
}).strict();
const GOOGLE_OAUTH_BINDING_COOKIE = 'pig_google_oauth';

export function createGoogleSheetsRoutes(
  db: Database,
  config: GoogleSheetsConfig,
  fetchImpl: typeof fetch = fetch,
): Hono<ApiEnv> {
  const routes = new Hono<ApiEnv>();
  const service = new GoogleSheetsService(db, config, fetchImpl);

  routes.get('/oauth/google/callback', async (context) => {
    const state = context.req.query('state');
    const code = context.req.query('code');
    const denied = context.req.query('error');
    const browserBinding = cookieValue(
      context.req.header('cookie'),
      oauthBindingCookieName(config.publicUrl),
    );
    context.header('Set-Cookie', oauthBindingCookie('', config.publicUrl, 0));
    if (denied) return context.redirect(`${config.publicUrl}/imports?google=denied`);
    if (!state || !code || !browserBinding) return context.text('Invalid Google authorization response.', 400);
    try {
      await service.completeOAuth({ state, code, browserBinding });
      return context.redirect(`${config.publicUrl}/imports?google=connected`);
    } catch {
      // OAuth codes, state, token bodies, and provider diagnostics never enter a redirect or log.
      return context.redirect(`${config.publicUrl}/imports?google=failed`);
    }
  });

  /*
   * Two capabilities, not one.
   *
   * Handing PIG a long-lived Google refresh token is `integration:connect`:
   * an authority over a third-party account, granted once, revocable
   * separately. Reading the resulting spreadsheets in order to import them is
   * `data:import`. Someone allowed to connect their Drive is not thereby
   * allowed to rewrite the book from it, and the reverse is just as true.
   *
   * `data:import` is any-team here because no import entity has been chosen
   * yet — the browser is still picking a file. The team is enforced at commit,
   * in imports.ts, where the target is known.
   */
  routes.use('/api/imports/google/*', async (context, next) => {
    const path = new URL(context.req.url).pathname;
    const managesConnection =
      path === '/api/imports/google/status' ||
      path === '/api/imports/google/connect' ||
      path === '/api/imports/google/connection';
    requireAnyTeamCapability(
      context.get('principal'),
      managesConnection ? 'integration:connect' : 'data:import',
    );
    await next();
  });
  routes.get('/api/imports/google/status', async (context) =>
    context.json(await service.connectionMetadata(context.get('principal').userId)));
  routes.post('/api/imports/google/connect', async (context) => {
    const started = await service.beginOAuth(context.get('principal'));
    context.header('Set-Cookie', oauthBindingCookie(started.browserBinding, config.publicUrl, 600));
    return context.json({ authorizationUrl: started.authorizationUrl });
  });
  routes.delete('/api/imports/google/connection', async (context) => {
    await service.disconnect(context.get('principal'));
    return context.body(null, 204);
  });
  routes.get('/api/imports/google/files', async (context) => {
    const parsed = listSchema.safeParse({
      pageToken: context.req.query('pageToken'),
      search: context.req.query('search'),
    });
    if (!parsed.success) throw new MutationError('invalid_google_page', 'Invalid Google Drive page request.', 400);
    try {
      return context.json(await service.listSpreadsheets(context.get('principal').userId, parsed.data));
    } catch (error) {
      throw publicGoogleError(error);
    }
  });
  routes.get('/api/imports/google/spreadsheets/:id/sheets', async (context) => {
    const spreadsheetId = identifier.safeParse(context.req.param('id'));
    if (!spreadsheetId.success) throw new MutationError('invalid_google_spreadsheet', 'Invalid spreadsheet ID.', 400);
    try {
      return context.json(await service.spreadsheetMetadata(context.get('principal').userId, spreadsheetId.data));
    } catch (error) {
      throw publicGoogleError(error);
    }
  });
  routes.post('/api/imports/google/table', async (context) => {
    const parsed = tableSchema.safeParse(await context.req.json());
    if (!parsed.success) throw new MutationError('invalid_google_table', 'Invalid Google Sheets selection.', 400);
    try {
      return context.json(await service.readTable(context.get('principal').userId, parsed.data));
    } catch (error) {
      throw publicGoogleError(error);
    }
  });
  return routes;
}

function oauthBindingCookieName(publicUrl: string): string {
  return new URL(publicUrl).protocol === 'https:'
    ? `__Host-${GOOGLE_OAUTH_BINDING_COOKIE}`
    : GOOGLE_OAUTH_BINDING_COOKIE;
}

function oauthBindingCookie(value: string, publicUrl: string, maxAge: number): string {
  const secure = new URL(publicUrl).protocol === 'https:' ? '; Secure' : '';
  return `${oauthBindingCookieName(publicUrl)}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAge}${secure}`;
}

function cookieValue(header: string | undefined, name: string): string | null {
  for (const part of header?.split(';') ?? []) {
    const [key, ...rest] = part.trim().split('=');
    if (key === name) return decodeURIComponent(rest.join('='));
  }
  return null;
}

function publicGoogleError(error: unknown): MutationError {
  if (error instanceof MutationError) return error;
  if (error instanceof GoogleApiError) {
    return new MutationError(
      error.reconnectRequired ? 'google_reconnect_required' : 'google_api_error',
      error.message,
      error.status >= 500 ? 502 : error.status === 404 ? 404 : 409,
    );
  }
  return new MutationError('google_api_error', 'Google Sheets could not complete the request.', 502);
}
