import { createHmac, randomBytes, timingSafeEqual } from 'crypto';
import { cookies } from 'next/headers';
import type { NextResponse } from 'next/server';
import { z } from 'zod';
import { SessionUserSchema } from '@nexumi/shared';
import { env } from './env';
import { redis } from './redis';

export const SESSION_COOKIE_NAME = 'nexumi_session';
const SESSION_KEY_PREFIX = 'webui:session:';
const SESSION_TTL_SECONDS = 60 * 60 * 24 * 7;

const SessionPayloadSchema = z.object({
  user: SessionUserSchema,
  accessToken: z.string().min(1),
  refreshToken: z.string().min(1).optional(),
  tokenExpiresAt: z.number().optional()
});

export type SessionPayload = z.infer<typeof SessionPayloadSchema>;

export class SessionRequiredError extends Error {
  constructor() {
    super('Session required');
    this.name = 'SessionRequiredError';
  }
}

function sign(value: string): string {
  return createHmac('sha256', env.SESSION_SECRET).update(value).digest('hex');
}

function buildCookieValue(sessionId: string): string {
  return `${sessionId}.${sign(sessionId)}`;
}

function parseCookieValue(cookieValue: string): string | null {
  const separatorIndex = cookieValue.lastIndexOf('.');
  if (separatorIndex <= 0) {
    return null;
  }
  const sessionId = cookieValue.slice(0, separatorIndex);
  const signature = cookieValue.slice(separatorIndex + 1);
  const expected = sign(sessionId);
  const provided = Buffer.from(signature);
  const expectedBuffer = Buffer.from(expected);
  if (provided.length !== expectedBuffer.length || !timingSafeEqual(provided, expectedBuffer)) {
    return null;
  }
  return sessionId;
}

function sessionRedisKey(sessionId: string): string {
  return `${SESSION_KEY_PREFIX}${sessionId}`;
}

/** Secure cookies only when the public WebUI URL is HTTPS (HTTP IP deploys must not use Secure). */
export function sessionCookieOptions(): {
  httpOnly: boolean;
  sameSite: 'lax';
  secure: boolean;
  path: string;
  maxAge: number;
} {
  return {
    httpOnly: true,
    sameSite: 'lax',
    secure: env.WEBUI_URL.startsWith('https://'),
    path: '/',
    maxAge: SESSION_TTL_SECONDS
  };
}

export function applySessionCookie(response: NextResponse, cookieValue: string): void {
  response.cookies.set(SESSION_COOKIE_NAME, cookieValue, sessionCookieOptions());
}

export function clearSessionCookie(response: NextResponse): void {
  response.cookies.set(SESSION_COOKIE_NAME, '', {
    ...sessionCookieOptions(),
    maxAge: 0
  });
}

/** Persists the session in Redis and returns the signed cookie value to attach to a response. */
export async function createSession(payload: SessionPayload): Promise<string> {
  const sessionId = randomBytes(32).toString('hex');
  await redis.set(sessionRedisKey(sessionId), JSON.stringify(payload), 'EX', SESSION_TTL_SECONDS);
  return buildCookieValue(sessionId);
}

async function readSessionIdFromCookie(): Promise<string | null> {
  const cookieStore = await cookies();
  const raw = cookieStore.get(SESSION_COOKIE_NAME)?.value;
  if (!raw) {
    return null;
  }
  return parseCookieValue(raw);
}

export async function getSession(): Promise<SessionPayload | null> {
  const sessionId = await readSessionIdFromCookie();
  if (!sessionId) {
    return null;
  }
  const stored = await redis.get(sessionRedisKey(sessionId));
  if (!stored) {
    return null;
  }
  try {
    const parsed = SessionPayloadSchema.safeParse(JSON.parse(stored));
    return parsed.success ? parsed.data : null;
  } catch {
    await redis.del(sessionRedisKey(sessionId));
    return null;
  }
}

/** Overwrites the current session payload in Redis (same session id / cookie). */
export async function updateSession(payload: SessionPayload): Promise<void> {
  const sessionId = await readSessionIdFromCookie();
  if (!sessionId) {
    return;
  }
  await redis.set(sessionRedisKey(sessionId), JSON.stringify(payload), 'EX', SESSION_TTL_SECONDS);
}

export async function destroySession(): Promise<void> {
  const sessionId = await readSessionIdFromCookie();
  if (sessionId) {
    await redis.del(sessionRedisKey(sessionId));
  }
}

export async function requireSession(): Promise<SessionPayload> {
  const session = await getSession();
  if (!session) {
    throw new SessionRequiredError();
  }
  return session;
}
