import { FastifyInstance, FastifyRequest } from 'fastify';
import {
  VIBE_EVENT_TYPES,
  VibeSessionCoordinator,
  VibeSessionLifecycleError,
  VibeSessionNotFoundError,
  VibePlanNotFoundError,
} from '../services/vibe-session-coordinator.service.js';
import { VibeCalendarContext } from '../services/generators.service.js';

const DEFAULT_USER_ID = '00000000-0000-0000-0000-000000000000';
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/i;
const CLIENT_VIBE_EVENT_TYPES = VIBE_EVENT_TYPES.filter((type) => ![
  'session_started', 'session_resumed', 'session_ended', 'plan_published', 'track_served',
].includes(type));

type Reply = { code: (statusCode: number) => { send: (payload: unknown) => unknown } };
type Body = Record<string, unknown>;

/** This mirrors the rest of the application until authentication owns identity. */
export type VibeIdentityResolver = (request: FastifyRequest) => string | null;

function isObject(value: unknown): value is Body {
  return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function validUuid(value: unknown): value is string {
  return value === DEFAULT_USER_ID || (typeof value === 'string' && UUID_RE.test(value));
}

function requestUser(request: FastifyRequest, resolveIdentity?: VibeIdentityResolver): string | null {
  const resolved = resolveIdentity?.(request);
  if (resolved !== undefined) return validUuid(resolved) ? resolved : null;
  const header = request.headers['x-user-id'];
  const userId = typeof header === 'string' && header ? header : DEFAULT_USER_ID;
  return validUuid(userId) ? userId : null;
}

function validOccurredAt(value: unknown): value is string {
  return typeof value === 'string' && ISO_TIMESTAMP_RE.test(value) && !Number.isNaN(Date.parse(value));
}

function parseCalendarContext(value: unknown): VibeCalendarContext | undefined | null {
  if (value === undefined) return undefined;
  if (!isObject(value)) return null;
  const { localHour, weekday, month, timeZone } = value;
  if (typeof localHour !== 'number' || !Number.isInteger(localHour) || localHour < 0 || localHour > 23) return null;
  if (typeof weekday !== 'number' || !Number.isInteger(weekday) || weekday < 0 || weekday > 6) return null;
  if (typeof month !== 'number' || !Number.isInteger(month) || month < 1 || month > 12) return null;
  if (timeZone !== undefined && (typeof timeZone !== 'string' || timeZone.length > 64)) return null;
  return { localHour, weekday, month, ...(typeof timeZone === 'string' ? { timeZone } : {}) };
}

function validationError(reply: Reply, message: string) {
  return reply.code(400).send({ error: message });
}

function sessionIdFrom(request: FastifyRequest, reply: Reply): string | null {
  const { sessionId } = request.params as { sessionId: string };
  return validUuid(sessionId) ? sessionId : (validationError(reply, 'sessionId must be a UUID'), null);
}

function parseStart(body: unknown):
  | { seedTrackId?: string; resumeSessionId?: string; context?: VibeCalendarContext }
  | { error: string } {
  const input = isObject(body) ? body : {};
  if (input.resumeSessionId !== undefined && !validUuid(input.resumeSessionId)) return { error: 'resumeSessionId must be a UUID' };
  if (input.seedTrackId !== undefined && !validUuid(input.seedTrackId)) return { error: 'seedTrackId must be a UUID' };
  if (input.resumeSessionId !== undefined && input.seedTrackId !== undefined) return { error: 'resumeSessionId cannot be combined with seedTrackId' };
  const context = parseCalendarContext(input.context);
  if (context === null) return { error: 'context must contain valid localHour, weekday, month, and optional timeZone' };
  return {
    seedTrackId: input.seedTrackId as string | undefined,
    resumeSessionId: input.resumeSessionId as string | undefined,
    ...(context ? { context } : {}),
  };
}

function parseEvent(body: unknown):
  | { type: typeof CLIENT_VIBE_EVENT_TYPES[number]; eventId?: string; trackId?: string; occurredAt?: Date; positionMs?: number; durationMs?: number; payload?: Body }
  | { error: string } {
  if (!isObject(body)) return { error: 'event body must be an object' };
  if (!CLIENT_VIBE_EVENT_TYPES.includes(body.type as typeof CLIENT_VIBE_EVENT_TYPES[number])) return { error: 'type must be a supported client Vibe event type' };
  if (body.eventId !== undefined && !validUuid(body.eventId)) return { error: 'eventId must be a UUID' };
  if (body.trackId !== undefined && !validUuid(body.trackId)) return { error: 'trackId must be a UUID' };
  if (body.occurredAt !== undefined && !validOccurredAt(body.occurredAt)) return { error: 'occurredAt must be an ISO-8601 timestamp' };
  if (body.positionMs !== undefined && (typeof body.positionMs !== 'number' || !Number.isInteger(body.positionMs) || body.positionMs < 0)) return { error: 'positionMs must be a non-negative integer' };
  if (body.durationMs !== undefined && (typeof body.durationMs !== 'number' || !Number.isInteger(body.durationMs) || body.durationMs < 0)) return { error: 'durationMs must be a non-negative integer' };
  if (body.payload !== undefined && !isObject(body.payload)) return { error: 'payload must be an object' };
  return {
    type: body.type as typeof CLIENT_VIBE_EVENT_TYPES[number],
    eventId: body.eventId as string | undefined,
    trackId: body.trackId as string | undefined,
    occurredAt: body.occurredAt === undefined ? undefined : new Date(body.occurredAt as string),
    positionMs: body.positionMs as number | undefined,
    durationMs: body.durationMs as number | undefined,
    payload: body.payload as Body | undefined,
  };
}

function parseAdvance(body: unknown):
  | { expectedPlanVersion?: number; unplayable?: { eventId: string; planVersionId: string; ordinal: number; trackId: string } }
  | { error: string } {
  const input = isObject(body) ? body : {};
  const version = input.expectedPlanVersion;
  if (version !== undefined && (typeof version !== 'number' || !Number.isInteger(version) || version < 1)) return { error: 'expectedPlanVersion must be a positive integer' };
  if (input.unplayable === undefined) return { expectedPlanVersion: version as number | undefined };
  if (!isObject(input.unplayable)) return { error: 'unplayable must be an object' };
  const unplayable = input.unplayable;
  if (version === undefined) return { error: 'expectedPlanVersion is required when advancing an unplayable item' };
  if (!validUuid(unplayable.eventId) || !validUuid(unplayable.planVersionId) || !validUuid(unplayable.trackId) || typeof unplayable.ordinal !== 'number' || !Number.isInteger(unplayable.ordinal) || unplayable.ordinal < 0) {
    return { error: 'unplayable requires UUID eventId, planVersionId, trackId and a non-negative integer ordinal' };
  }
  return { expectedPlanVersion: version as number, unplayable: unplayable as { eventId: string; planVersionId: string; ordinal: number; trackId: string } };
}

export default async function vibeSessionsRoutes(
  fastify: FastifyInstance,
  options: { coordinator: VibeSessionCoordinator; identityResolver?: VibeIdentityResolver },
) {
  const { coordinator, identityResolver } = options;
  const userFor = (request: FastifyRequest, reply: Reply) => {
    const userId = requestUser(request, identityResolver);
    return userId ? userId : (validationError(reply, 'x-user-id must be a UUID'), null);
  };

  fastify.post('/v2/vibe/sessions', async (request, reply) => {
    const userId = userFor(request, reply);
    const input = parseStart(request.body);
    if (!userId || 'error' in input) return 'error' in input ? validationError(reply, input.error) : undefined;
    try {
      return reply.code(201).send(await coordinator.start(userId, input));
    } catch (error) {
      return sendCoordinatorError(reply, error);
    }
  });

  fastify.get('/v2/vibe/sessions/:sessionId/plans', async (request, reply) => {
    const userId = userFor(request, reply);
    const sessionId = sessionIdFrom(request, reply);
    const { version } = request.query as { version?: string };
    const parsedVersion = version === undefined ? undefined : Number(version);
    if (!userId || !sessionId) return;
    if (version !== undefined && (!Number.isInteger(parsedVersion) || parsedVersion! < 1)) return validationError(reply, 'version must be a positive integer');
    try {
      return reply.send(await coordinator.getPlan(userId, sessionId, parsedVersion));
    } catch (error) {
      return sendCoordinatorError(reply, error);
    }
  });

  fastify.post('/v2/vibe/sessions/:sessionId/events', async (request, reply) => {
    const userId = userFor(request, reply);
    const sessionId = sessionIdFrom(request, reply);
    const input = parseEvent(request.body);
    if (!userId || !sessionId || 'error' in input) return 'error' in input ? validationError(reply, input.error) : undefined;
    try {
      return reply.send(await coordinator.appendEvent(userId, sessionId, input));
    } catch (error) {
      return sendCoordinatorError(reply, error);
    }
  });

  fastify.post('/v2/vibe/sessions/:sessionId/end', async (request, reply) => {
    const userId = userFor(request, reply);
    const sessionId = sessionIdFrom(request, reply);
    if (!userId || !sessionId) return;
    try {
      return reply.send(await coordinator.end(userId, sessionId));
    } catch (error) {
      return sendCoordinatorError(reply, error);
    }
  });

  fastify.post('/v2/vibe/sessions/:sessionId/advance', async (request, reply) => {
    const userId = userFor(request, reply);
    const sessionId = sessionIdFrom(request, reply);
    const input = parseAdvance(request.body);
    if (!userId || !sessionId || 'error' in input) return 'error' in input ? validationError(reply, input.error) : undefined;
    try {
      return reply.send(input.unplayable
        ? await coordinator.advancePastUnplayable(userId, sessionId, { expectedPlanVersion: input.expectedPlanVersion!, ...input.unplayable })
        : input.expectedPlanVersion === undefined
          ? await coordinator.serveNext(userId, sessionId)
          : await coordinator.serveNext(userId, sessionId, input.expectedPlanVersion));
    } catch (error) {
      return sendCoordinatorError(reply, error);
    }
  });
}

function sendCoordinatorError(reply: Reply, error: unknown) {
  if (error instanceof VibeSessionNotFoundError || error instanceof VibePlanNotFoundError) return reply.code(404).send({ error: error.message });
  if (error instanceof VibeSessionLifecycleError) return reply.code(409).send({ error: error.message, code: 'VIBE_SESSION_NOT_ACTIVE' });
  throw error;
}
