import { PIGGY_PAGE_ROUTES, PIGGY_RECORD_TYPES } from '@pig/core';
import type { Database } from '@pig/db';
import { Hono } from 'hono';
import { stream } from 'hono/streaming';
import { z } from 'zod';
import type { Config } from '../lib/config';
import type { ApiEnv } from '../lib/mutation';
import { ensurePlatformSettings } from './admin-settings';

/**
 * Derived from the @pig/core tuples, and kept in step with the identical
 * schema in the Piggy chat server. Both are `.strict()`, so a context arm
 * missing from either one is a 400 at that hop rather than a degraded answer.
 */
const contextSchema = z.discriminatedUnion('type', [
  z
    .object({
      type: z.enum(PIGGY_RECORD_TYPES),
      id: z.string().uuid(),
      label: z.string().max(240).optional(),
    })
    .strict(),
  z
    .object({
      type: z.literal('page'),
      route: z.enum(PIGGY_PAGE_ROUTES),
      label: z.string().max(240).optional(),
    })
    .strict(),
]);

const requestSchema = z
  .object({
    message: z.string().trim().min(1).max(4_000),
    history: z
      .array(
        z.object({
          role: z.enum(['user', 'assistant']),
          content: z.string().min(1).max(8_000),
        }),
      )
      .max(20)
      .optional(),
    context: contextSchema.optional(),
  })
  .strict();

export interface PiggyChatProxyOptions {
  enabled: boolean;
  internalUrl?: string;
  internalToken?: string;
  fetchImpl?: typeof fetch;
  /**
   * The admin toggle, read per request. Omitted, the environment gate alone
   * decides — which is what shipped, and why turning Piggy off in the admin UI
   * did nothing.
   */
  resolvePiggyEnabled?: () => Promise<boolean>;
}

/** The stored toggle. Paired with `createPiggyChatRoutes` at composition. */
export function platformPiggyEnabled(config: Config, db: Database): () => Promise<boolean> {
  return async () => (await ensurePlatformSettings(config, db)).piggyEnabled;
}

export function createPiggyChatRoutes(options: PiggyChatProxyOptions) {
  const routes = new Hono<ApiEnv>();
  const fetchImpl = options.fetchImpl ?? fetch;
  // Configuration cannot change under a running process; the toggle can.
  const configured = Boolean(options.enabled && options.internalUrl && options.internalToken);

  /**
   * The environment variable is the outer gate and the stored setting the
   * inner one: an operator who has not provisioned Piggy cannot have it
   * switched on from the admin UI. A failed settings read falls back to the
   * outer gate rather than 503-ing every dock on the site over one bad query.
   */
  async function isAvailable(): Promise<boolean> {
    if (!configured) return false;
    if (!options.resolvePiggyEnabled) return true;
    try {
      return await options.resolvePiggyEnabled();
    } catch {
      return true;
    }
  }

  routes.get('/api/piggy/status', async (c) => {
    const principal = c.get('principal');
    const available = await isAvailable();
    return c.json({
      enabled: available,
      canUse: available && principal.scopes.includes('read'),
    });
  });

  routes.post('/api/piggy/chat', async (c) => {
    const principal = c.get('principal');
    if (!principal.scopes.includes('read')) {
      return c.json(
        { error: "This credential lacks the 'read' scope.", code: 'insufficient_scope' },
        403,
      );
    }
    if (!(await isAvailable()) || !options.internalUrl || !options.internalToken) {
      return c.json({ error: 'Piggy chat is not available.', code: 'piggy_unavailable' }, 503);
    }

    let raw: unknown;
    try {
      raw = await c.req.json();
    } catch {
      return c.json({ error: 'Request body must be valid JSON.', code: 'invalid_json' }, 400);
    }
    const parsed = requestSchema.safeParse(raw);
    if (!parsed.success) {
      return c.json(
        { error: 'Invalid Piggy chat request.', code: 'invalid_request', issues: parsed.error.issues },
        400,
      );
    }

    const upstream = await fetchImpl(
      `${options.internalUrl.replace(/\/$/, '')}/internal/chat`,
      {
        method: 'POST',
        headers: {
          authorization: `Bearer ${options.internalToken}`,
          'content-type': 'application/json',
          accept: 'application/x-ndjson',
        },
        body: JSON.stringify({ principalUserId: principal.userId, ...parsed.data }),
        signal: c.req.raw.signal,
      },
    );

    if (!upstream.ok) {
      await upstream.body?.cancel().catch(() => {});
      return c.json(
        {
          error: 'Piggy chat service did not respond.',
          code: 'piggy_upstream_error',
        },
        502,
      );
    }
    const upstreamBody = upstream.body;
    if (!upstreamBody) {
      return c.json(
        {
          error: 'Piggy chat service returned no response stream.',
          code: 'piggy_upstream_error',
        },
        502,
      );
    }

    c.header('content-type', 'application/x-ndjson; charset=utf-8');
    c.header('cache-control', 'no-cache, no-transform');
    c.header('x-accel-buffering', 'no');
    return stream(c, async (output) => {
      const reader = upstreamBody.getReader();
      try {
        while (true) {
          const { done, value } = await reader.read();
          if (done) return;
          await output.write(value);
        }
      } finally {
        reader.releaseLock();
      }
    });
  });

  return routes;
}
