import { NextResponse } from 'next/server';
import { getSession, SessionPayload } from '@server/auth';
import { AuthError, ForbiddenError } from '@server/errors';
import { RouteContext, RouteHandler } from './withErrorHandler';
import { DEMO_MODE_ENABLED, DEMO_OWNER_EMAIL, DEMO_ADMIN_EMAIL } from '@/lib/demoMode';
import { db } from '@server/db/drizzle';
import { sql } from 'drizzle-orm';
import { updateRequestContext } from '@server/logger/context';

export type AuthedRequest = Request & { session: SessionPayload };

export type AuthedHandler = (
  req: AuthedRequest,
  context: RouteContext
) => Promise<NextResponse | Response>;

const READ_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);

// Endpoints that must keep working for demo sessions even though they mutate
// (sign-out so demo users can leave, seed so the demo can self-rebootstrap,
// gate/me which carry only session-state side-effects, sign-in for re-auth).
// Exact-path match only — prefix matching would silently whitelist nested
// mutating routes like /api/auth/me/language.
const DEMO_WRITE_WHITELIST = new Set<string>([
  '/api/auth/sign-in',
  '/api/auth/sign-out',
  '/api/auth/seed-demo-users',
  '/api/auth/gate',
  '/api/auth/me',
]);

const DEMO_EMAILS = new Set<string>([DEMO_OWNER_EMAIL, DEMO_ADMIN_EMAIL]);

const DEMO_READ_ONLY_BODY = {
  error: 'Demo mode is read-only — sign up for a free trial to make changes.',
  code: 'DEMO_READ_ONLY',
} as const;

/**
 * Routes that are technically "writes" but must remain available to demo users
 * because the action does not mutate the demo restaurant's data.
 *
 * Note: fully unauthenticated public routes (e.g. /api/public/**, password-reset)
 * never pass through `withAuth`, so they don't need to appear here.
 */
function isDemoBlocked(req: Request, session: SessionPayload): boolean {
  if (!DEMO_MODE_ENABLED) return false;
  if (!DEMO_EMAILS.has(session.email)) return false;
  if (READ_METHODS.has(req.method.toUpperCase())) return false;
  let pathname: string;
  try {
    pathname = new URL(req.url).pathname;
  } catch {
    // Fail closed: if we cannot parse the URL we cannot prove the path is on
    // the whitelist, so block the write.
    return true;
  }
  if (DEMO_WRITE_WHITELIST.has(pathname)) return false;
  return true;
}

/**
 * Structural minimum that both AuthedRequest and AuthedParsedRequest<T>
 * (which is AuthedRequest & { parsedBody: T }) satisfy. Using this as the
 * parameter type removes the need for any casts at call sites.
 */
type WithSession = { session: SessionPayload };

/**
 * Throws ForbiddenError (403) if the current staff member lacks `action`
 * permission for `section`. Owners, superadmins and support accounts bypass
 * the check. Any non-privileged user without a matching staff row is denied
 * (fail-closed) because the absence of a row is not proof of ownership.
 */
export async function requireSection(
  req: WithSession,
  section: string,
  action: 'create' | 'read' | 'update' | 'delete' = 'read'
): Promise<void> {
  const { role, userId, restaurantId } = req.session;
  if (!restaurantId) return;
  if (role === 'owner' || role === 'superadmin' || role === 'support') return;
  const { rows } = await db.execute(sql`
    SELECT permissions FROM staff WHERE user_id = ${userId} AND restaurant_id = ${restaurantId} LIMIT 1
  `);
  const row = rows[0] as { permissions?: Record<string, Record<string, boolean>> } | undefined;
  if (!row || !row.permissions?.[section]?.[action]) {
    throw new ForbiddenError(`You do not have ${action} access to the ${section} section`);
  }
}

/**
 * Like requireSection, but allows access if the staff member has `action`
 * permission for ANY of the listed sections. Useful when a single API
 * endpoint backs two different sidebar sections (e.g. storefront + branches).
 * Owners, superadmins and support accounts always bypass the check.
 * Fail-closed: a missing staff row is treated as denial, not bypass.
 */
export async function requireSectionAny(
  req: WithSession,
  sections: string[],
  action: 'create' | 'read' | 'update' | 'delete' = 'read'
): Promise<void> {
  const { role, userId, restaurantId } = req.session;
  if (!restaurantId) return;
  if (role === 'owner' || role === 'superadmin' || role === 'support') return;
  const { rows } = await db.execute(sql`
    SELECT permissions FROM staff WHERE user_id = ${userId} AND restaurant_id = ${restaurantId} LIMIT 1
  `);
  const row = rows[0] as { permissions?: Record<string, Record<string, boolean>> } | undefined;
  if (!row || !sections.some(s => row.permissions?.[s]?.[action])) {
    throw new ForbiddenError(`You do not have ${action} access to this section`);
  }
}

export function withAuth(handler: AuthedHandler | RouteHandler): RouteHandler {
  return async (req, context) => {
    const session = await getSession();

    if (!session) {
      const err = new AuthError();
      return NextResponse.json(err.toJSON(), { status: err.statusCode });
    }

    if (isDemoBlocked(req, session)) {
      return NextResponse.json(DEMO_READ_ONLY_BODY, { status: 403 });
    }

    // Backfill pinnedBranchId for legacy JWTs issued before the field existed
    // (sign-in always sets it explicitly, so newly-issued tokens skip this).
    // Owners are never pinned even if users.branch_id is set; lookup failure
    // fails closed.
    if (session.pinnedBranchId === undefined && session.userId) {
      try {
        const { rows } = await db.execute(sql`
          SELECT role, branch_id FROM users WHERE id = ${session.userId} LIMIT 1
        `);
        const row = rows[0] as { role?: string | null; branch_id?: string | null } | undefined;
        const dbRole = row?.role ?? session.role;
        session.pinnedBranchId = dbRole === 'owner' ? null : (row?.branch_id ?? null);
        if (row?.role) session.role = row.role;
      } catch {
        const err = new AuthError();
        return NextResponse.json(err.toJSON(), { status: err.statusCode });
      }
    }

    Object.defineProperty(req, 'session', {
      value: session,
      writable: false,
      enumerable: true,
      configurable: true,
    });

    updateRequestContext({
      restaurantId: session.restaurantId ?? null,
      userId: session.userId ?? null,
    });

    return handler(req as AuthedRequest, context);
  };
}
