import { redirect } from 'next/navigation';
import { NextResponse } from 'next/server';
import { ZodError } from 'zod';
import { hasManageGuildPermission } from '@nexumi/shared';
import { fetchDiscordUserGuildsCached, refreshDiscordToken } from './discord-oauth';
import { BadRequestError, UpstreamError } from './api-errors';
import { hasOwnerGuildBypass } from './owner-auth';
import { prisma } from './prisma';
import {
  getSession,
  SessionRequiredError,
  updateSession,
  type SessionPayload
} from './session';

export { BadRequestError, UpstreamError } from './api-errors';

export class UnauthorizedError extends Error {
  constructor(message = 'Authentication required') {
    super(message);
    this.name = 'UnauthorizedError';
  }
}

export class ForbiddenError extends Error {
  constructor(message = 'You do not have access to this resource') {
    super(message);
    this.name = 'ForbiddenError';
  }
}

const TOKEN_REFRESH_SKEW_MS = 60_000;

/**
 * Refreshes the Discord access token when expired (or about to expire).
 * Throws {@link UnauthorizedError} when refresh is impossible.
 */
export async function ensureFreshSession(session: SessionPayload): Promise<SessionPayload> {
  const expiresAt = session.tokenExpiresAt ?? 0;
  if (expiresAt > Date.now() + TOKEN_REFRESH_SKEW_MS) {
    return session;
  }

  if (!session.refreshToken) {
    throw new UnauthorizedError('Session expired – please log in again');
  }

  try {
    const token = await refreshDiscordToken(session.refreshToken);
    const next: SessionPayload = {
      ...session,
      accessToken: token.access_token,
      refreshToken: token.refresh_token || session.refreshToken,
      tokenExpiresAt: Date.now() + token.expires_in * 1000
    };
    await updateSession(next);
    return next;
  } catch {
    throw new UnauthorizedError('Session expired – please log in again');
  }
}

/**
 * Use in API routes. Throws {@link UnauthorizedError} when no session exists.
 */
export async function requireAuth(): Promise<SessionPayload> {
  const session = await getSession();
  if (!session) {
    throw new UnauthorizedError();
  }
  return ensureFreshSession(session);
}

/**
 * Use in Server Components / layouts. Redirects to /login instead of throwing,
 * since there is no JSON response to return.
 */
export async function requireAuthOrRedirect(): Promise<SessionPayload> {
  const session = await getSession();
  if (!session) {
    redirect('/login');
  }
  try {
    return await ensureFreshSession(session);
  } catch {
    redirect('/login');
  }
}

/**
 * Ensures the current session user has Manage Guild permission on the given
 * guild (via Discord OAuth guild list) and that the bot is actually installed
 * there (tracked via the `Guild` table). Owner-panel users with SUPPORT+
 * bypass Discord membership checks. Use in API routes.
 */
export async function requireGuildAccess(guildId: string): Promise<SessionPayload> {
  const session = await requireAuth();
  const dbGuild = await prisma.guild.findUnique({ where: { id: guildId } });
  if (!dbGuild) {
    throw new ForbiddenError('Nexumi is not installed on this server');
  }

  if (await hasOwnerGuildBypass(session.user.id)) {
    return session;
  }

  try {
    const guilds = await fetchDiscordUserGuildsCached(session.accessToken);
    const guild = guilds.find((entry) => entry.id === guildId);
    if (!guild || !hasManageGuildPermission(guild.permissions)) {
      throw new ForbiddenError('Missing Manage Guild permission for this server');
    }
    return session;
  } catch (error) {
    if (error instanceof ForbiddenError) {
      throw error;
    }
    throw new UnauthorizedError('Discord session expired – please log in again');
  }
}

/**
 * Same as {@link requireGuildAccess} but for Server Components: redirects
 * instead of throwing.
 */
export async function requireGuildAccessOrRedirect(guildId: string): Promise<SessionPayload> {
  const session = await requireAuthOrRedirect();
  const dbGuild = await prisma.guild.findUnique({ where: { id: guildId } });
  if (!dbGuild) {
    redirect('/dashboard');
  }

  if (await hasOwnerGuildBypass(session.user.id)) {
    return session;
  }

  try {
    const guilds = await fetchDiscordUserGuildsCached(session.accessToken);
    const guild = guilds.find((entry) => entry.id === guildId);
    if (!guild || !hasManageGuildPermission(guild.permissions)) {
      redirect('/dashboard');
    }
    return session;
  } catch {
    redirect('/login');
  }
}

export function toApiErrorResponse(error: unknown): NextResponse {
  if (error instanceof UnauthorizedError || error instanceof SessionRequiredError) {
    return NextResponse.json({ error: error.message }, { status: 401 });
  }
  if (error instanceof ForbiddenError) {
    return NextResponse.json({ error: error.message }, { status: 403 });
  }
  if (error instanceof BadRequestError) {
    return NextResponse.json({ error: error.message }, { status: 400 });
  }
  if (error instanceof UpstreamError) {
    return NextResponse.json({ error: error.message }, { status: error.status });
  }
  if (error instanceof ZodError) {
    return NextResponse.json(
      { error: 'Validation failed', issues: error.issues },
      { status: 400 }
    );
  }
  console.error('[api]', error);
  return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
