import type {NextRequest} from 'next/server';
import {getServerSession} from '@/features/auth/services/serverSession';
import type {ApiErrorCode, ApiFailure, ApiSuccess, RangeKey} from '@/shared/types/api';

const RANGES: RangeKey[] = ['7d', '30d', '90d', 'mtd', 'ytd'];

export interface Query {
  range: RangeKey;
  storeId: string;
  /** Dev-only forced state. */
  state?: 'error' | 'empty' | 'loading';
  delayMs: number;
  /**
   * Request time, resolved once per request and threaded through every
   * generator. Fixtures must never call Date.now() themselves, or two panels
   * in the same render would disagree about what "today" is.
   */
  nowMs: number;
}

/**
 * The authorisation check every data route starts with.
 *
 * Returns a 401 Response to bail out with, or null to continue:
 *
 *   const denied = await requireApiSession();
 *   if (denied) return denied;
 *
 * ── Why this exists when the proxy already gates /api ─────────────────────
 * Next's own guidance is explicit that proxy coverage is not a substitute for
 * checking inside the handler: a matcher change, a route move or a Server
 * Function can all take a path out from under the proxy without anyone
 * noticing, and the failure mode is silent — data starts being served to
 * anonymous callers and nothing errors. This check cannot be routed around,
 * because it runs where the data is.
 */
export async function requireApiSession(): Promise<Response | null> {
  const session = await getServerSession();
  if (session) return null;
  return fail('unauthorized', 'Sign in to continue.', 401);
}

export function parseQuery(req: NextRequest): Query {
  const p = req.nextUrl.searchParams;
  const range = p.get('range');
  const delay = Number(p.get('_delay') ?? 0);
  const state = p.get('_state');

  return {
    range: RANGES.includes(range as RangeKey) ? (range as RangeKey) : '30d',
    storeId: p.get('storeId') || 'all',
    state:
      state === 'error' || state === 'empty' || state === 'loading'
        ? state
        : undefined,
    delayMs: Number.isFinite(delay) ? Math.min(Math.max(delay, 0), 15000) : 0,
    nowMs: Date.now(),
  };
}

export function ok<T>(data: T, q: Query): Response {
  const body: ApiSuccess<T> = {
    data,
    meta: {
      generatedAt: new Date(q.nowMs).toISOString(),
      range: q.range,
      storeId: q.storeId,
    },
  };
  return Response.json(body, {
    // Fixtures are deterministic but time-derived; never let a CDN pin them.
    headers: {'cache-control': 'no-store'},
  });
}

export function fail(
  code: ApiErrorCode,
  message: string,
  status = 500,
): Response {
  const body: ApiFailure = {error: {code, message}};
  return Response.json(body, {status, headers: {'cache-control': 'no-store'}});
}

/**
 * Dev-only state simulation — what makes "loading / empty / error are properly
 * handled" a checkable claim rather than an aspiration.
 *
 *   ?_state=error   → 500 ApiFailure
 *   ?_state=empty   → empty payload
 *   ?_delay=3000    → stall, to inspect skeletons
 *
 * Returns a Response to short-circuit with, or null to continue. Compiled out
 * of production behaviour by the NODE_ENV guard.
 */
export async function simulate(q: Query): Promise<Response | null> {
  if (process.env.NODE_ENV === 'production') return null;

  if (q.delayMs > 0) {
    await new Promise((r) => setTimeout(r, q.delayMs));
  }
  if (q.state === 'error') {
    return fail('internal', 'Simulated failure (?_state=error)');
  }
  if (q.state === 'loading') {
    await new Promise((r) => setTimeout(r, 60000));
  }
  return null;
}

/** True when the caller asked for an empty payload. */
export function wantsEmpty(q: Query): boolean {
  return process.env.NODE_ENV !== 'production' && q.state === 'empty';
}
