import { cookies } from "next/headers";
import { NextRequest } from "next/server";
import {
  db,
  AdminUser,
  AdminSession,
  AuditLocation,
  NetworkSecurityInfo,
  generateToken,
  hashToken,
} from "./db";
import { AdminPermission, userHasPermission } from "./permissions";
import {
  resolveClientIp,
  isPrivateOrLocalIp,
  getNetworkSecurity,
} from "./network-security";

import { parseUserAgent } from "./user-agent";

export const SESSION_COOKIE_NAME = "quto_admin_session";
const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24; // 24 hours

export interface AuthContext {
  user: AdminUser;
  session: AdminSession;
}

/**
 * Creates a new authenticated session for a user and stores it in the database.
 */
export async function createAdminSession(
  userId: string,
  clientMeta?: {
    ip?: string | null;
    userAgent?: string | null;
    location?: AuditLocation | null;
    networkSecurity?: NetworkSecurityInfo | null;
  }
): Promise<{ session: AdminSession; rawToken: string }> {
  const user = await db.getUserById(userId);
  if (!user) throw new Error("User not found");
  if (user.status !== "ACTIVE") {
    throw new Error("Cannot create session for inactive or suspended user.");
  }

  const rawToken = generateToken(32);
  const tokenHash = hashToken(rawToken);

  const settings = await db.getSettings();
  const timeoutMs = (settings.sessionTimeoutMinutes || 1440) * 60 * 1000;
  const now = new Date();
  const expiresAt = new Date(now.getTime() + timeoutMs).toISOString();

  const parsedUA = parseUserAgent(clientMeta?.userAgent);

  const session: AdminSession = {
    id: crypto.randomUUID(),
    userId,
    tokenHash,
    status: "ACTIVE",
    ipAddress: clientMeta?.ip || null,
    userAgent: clientMeta?.userAgent || null,
    device: parsedUA.summary,
    browser: parsedUA.browser,
    browserVersion: parsedUA.browserVersion,
    operatingSystem: parsedUA.operatingSystem,
    deviceType: parsedUA.deviceType,
    location: clientMeta?.location || null,
    networkSecurity: clientMeta?.networkSecurity || null,
    createdAt: now.toISOString(),
    lastActiveAt: now.toISOString(),
    lastSeenAt: now.toISOString(),
    expiresAt,
    revoked: false,
    revokedAt: null,
    revokedBy: null,
    revokeReason: null,
    revokedReason: null,
  };

  await db.createSession(session);
  return { session, rawToken };
}

/**
 * Attaches the session cookie to the current outgoing response.
 */
export async function setSessionCookie(rawToken: string): Promise<void> {
  const cookieStore = await cookies();
  cookieStore.set(SESSION_COOKIE_NAME, rawToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    path: "/",
    maxAge: SESSION_MAX_AGE_SECONDS,
  });
}

/**
 * Clears the session cookie.
 */
export async function clearSessionCookie(): Promise<void> {
  const cookieStore = await cookies();
  cookieStore.set(SESSION_COOKIE_NAME, "", {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    path: "/",
    maxAge: 0,
    expires: new Date(0),
  });
  cookieStore.delete(SESSION_COOKIE_NAME);
}

/**
 * Retrieves the raw session token from cookies or Authorization header.
 */
export async function extractRawToken(req?: NextRequest): Promise<string | null> {
  if (req) {
    const authHeader = req.headers.get("authorization");
    if (authHeader && authHeader.startsWith("Bearer ")) {
      return authHeader.substring(7).trim();
    }
    const cookieHeader = req.cookies.get(SESSION_COOKIE_NAME);
    if (cookieHeader?.value) {
      return cookieHeader.value;
    }
  }

  try {
    const cookieStore = await cookies();
    const tokenCookie = cookieStore.get(SESSION_COOKIE_NAME);
    return tokenCookie?.value || null;
  } catch {
    return null;
  }
}

/**
 * Resolves and validates the current admin session.
 */
export async function getAdminSession(req?: NextRequest): Promise<AuthContext | null> {
  const rawToken = await extractRawToken(req);
  if (!rawToken) return null;

  const tokenHash = hashToken(rawToken);
  const session = await db.getSessionByTokenHash(tokenHash);
  if (
    !session ||
    session.revoked ||
    session.status === "REVOKED" ||
    session.status === "EXPIRED" ||
    session.status === "BLOCKED"
  ) {
    return null;
  }

  const now = new Date();
  const nowIso = now.toISOString();

  // 1. Check absolute expiration
  if (session.expiresAt <= nowIso) {
    await db.revokeSession(tokenHash, "SYSTEM", "ABSOLUTE_TIMEOUT");
    return null;
  }

  // 2. Check inactivity expiration (stale session cleanup: 120 minutes)
  const lastSeenIso = session.lastSeenAt || session.lastActiveAt || session.createdAt;
  const lastSeenTime = new Date(lastSeenIso).getTime();
  const inactivityLimitMs = 120 * 60 * 1000; // 2 hours
  if (now.getTime() - lastSeenTime > inactivityLimitMs) {
    await db.revokeSession(tokenHash, "SYSTEM", "INACTIVITY_TIMEOUT");
    return null;
  }

  // 3. Check user validity
  const user = await db.getUserById(session.userId);
  if (!user) return null;

  // If user was suspended or deactivated, invalidate session immediately
  if (user.status !== "ACTIVE") {
    await db.revokeSession(tokenHash, "SYSTEM", `User is ${user.status}`);
    return null;
  }

  // 4. Check if session network security was marked blocked
  if (session.networkSecurity?.isBlocked) {
    await db.revokeSession(tokenHash, "SYSTEM", session.networkSecurity.blockReason || "Network security blocked");
    return null;
  }

  // 5. If request is provided, inspect for material IP migration
  if (req) {
    const currentIp = resolveClientIp(req);
    if (currentIp && session.ipAddress && currentIp !== session.ipAddress) {
      const netSec = await getNetworkSecurity(currentIp, req);
      await db.updateSessionNetworkSecurity(tokenHash, netSec);
      if (netSec.isBlocked) {
        await db.revokeSession(tokenHash, "SYSTEM", netSec.blockReason || "VPN/Proxy detected on network change");
        const meta = getClientMetadata(req);
        try {
          await db.createAuditLog({
            action: "NETWORK_SECURITY_BLOCKED",
            status: "FAILURE",
            userId: session.userId,
            targetType: "SESSION",
            targetId: session.id,
            details: {
              reason: "Disallowed network detected during session IP migration",
              network: netSec,
              previousIp: session.ipAddress,
              newIp: currentIp,
            },
            location: session.location || meta.location,
            networkSecurity: netSec,
            ipAddress: currentIp,
            userAgent: meta.userAgent,
          });
        } catch {}
        return null;
      }
    }
  }

  // 6. Update session touch (throttled to at most once every 60 seconds per session)
  if (!session.isLocked) {
    if (now.getTime() - lastSeenTime >= 60 * 1000) {
      await db.touchSession(tokenHash);
      session.lastSeenAt = nowIso;
      session.lastActiveAt = nowIso;
    }
  }

  return { user, session };
}

/**
 * Server-side guard: Requires an active, authenticated administrator.
 * Throws 401 error if unauthenticated or session invalid.
 * Throws 423 error if session is currently locked due to inactivity.
 */
export async function requireAuth(
  req?: NextRequest,
  options?: { allowLocked?: boolean }
): Promise<AuthContext> {
  const ctx = await getAdminSession(req);
  if (!ctx) {
    const error = new Error("Unauthorized: Invalid or expired session");
    (error as unknown as { status: number }).status = 401;
    throw error;
  }
  if (ctx.session.isLocked && !options?.allowLocked) {
    const error = new Error("Locked: Admin session is locked. Please enter your PIN, password, or passkey to unlock.");
    (error as unknown as { status: number }).status = 423;
    throw error;
  }
  return ctx;
}

/** Alias requiring an unlocked, active administrator session. */
export const requireUnlockedAdminSession = (req?: NextRequest) => requireAuth(req, { allowLocked: false });
export const requireUnlockedAdmin = (req?: NextRequest) => requireAuth(req, { allowLocked: false });
export const requireAdminSession = (req?: NextRequest, options?: { allowLocked?: boolean }) => requireAuth(req, options);
export const requireAuthenticatedAdmin = (req?: NextRequest, options?: { allowLocked?: boolean }) => requireAuth(req, options);

/**
 * Server-side guard: Requires that the authenticated user is the MAIN ADMIN.
 * Throws 403 Forbidden if the user is a Sub-Admin.
 */
export async function requireMainAdmin(
  req?: NextRequest,
  options?: { allowLocked?: boolean }
): Promise<AuthContext> {
  const ctx = await requireAuth(req, options);
  if (ctx.user.level !== "MAIN_ADMIN" && ctx.user.role !== "MAIN_ADMIN") {
    const error = new Error("Forbidden: This action requires Main Admin privileges.");
    (error as unknown as { status: number }).status = 403;
    throw error;
  }
  return ctx;
}

/**
 * Server-side guard: Requires a specific permission.
 * Throws 403 Forbidden if not authorized.
 */
export async function requirePermission(
  permission: AdminPermission,
  req?: NextRequest,
  options?: { allowLocked?: boolean }
): Promise<AuthContext> {
  const ctx = await requireAuth(req, options);
  if (ctx.user.level === "MAIN_ADMIN" || ctx.user.role === "MAIN_ADMIN") {
    return ctx;
  }
  let allowed = userHasPermission(ctx.user, permission);
  if (!allowed && ctx.user.role) {
    const roleRecord = await db.getRoleById(ctx.user.role);
    if (roleRecord && roleRecord.permissions.includes(permission)) {
      allowed = true;
    }
  }
  if (!allowed) {
    const error = new Error(`Forbidden: Missing required permission [${permission}].`);
    (error as unknown as { status: number }).status = 403;
    throw error;
  }
  return ctx;
}

export interface AdminAuditContext {
  user: AdminUser;
  session: AdminSession;
  location: AuditLocation | null;
  ip: string | null;
  userAgent: string | null;
  networkSecurity?: NetworkSecurityInfo | null;
}

/**
 * Resolves full authoritative audit context for admin mutations.
 * Combines authenticated user, active session, resolved IP, user agent,
 * and authoritative location (from header, cookie, or MongoDB session).
 */
export async function getAdminAuditContext(
  req?: NextRequest | Request,
  options?: { allowLocked?: boolean; permission?: AdminPermission }
): Promise<AdminAuditContext> {
  const ctx = options?.permission
    ? await requirePermission(options.permission, req as NextRequest, options)
    : await requireAuth(req as NextRequest, options);

  const meta = getClientMetadata(req);
  const location = meta.location || ctx.session.location || null;

  return {
    user: ctx.user,
    session: ctx.session,
    location,
    ip: meta.ip || ctx.session.ipAddress || null,
    userAgent: meta.userAgent || ctx.session.userAgent || null,
    networkSecurity: ctx.session.networkSecurity || null,
  };
}

/**
 * Helper to extract client IP address, User Agent, and location for audit logging.
 */
export function getClientMetadata(req?: NextRequest | Request): {
  ip: string | null;
  userAgent: string | null;
  location: AuditLocation | null;
} {
  if (!req) return { ip: null, userAgent: null, location: null };

  const headers = req.headers;
  const ip = resolveClientIp(req);
  const userAgent = headers.get("user-agent") || null;

  let location: AuditLocation | null = null;
  const locHeader = headers.get("x-admin-location");
  if (locHeader) {
    try {
      const parsed = JSON.parse(decodeURIComponent(locHeader));
      if (parsed && typeof parsed === "object") {
        location = {
          status:
            parsed.status === "available" || parsed.status === "denied" || parsed.status === "unavailable"
              ? parsed.status
              : "unavailable",
          latitude: typeof parsed.latitude === "number" ? parsed.latitude : undefined,
          longitude: typeof parsed.longitude === "number" ? parsed.longitude : undefined,
          address: typeof parsed.address === "string" ? parsed.address : undefined,
          locality: typeof parsed.locality === "string" ? parsed.locality : undefined,
          city: typeof parsed.city === "string" ? parsed.city : undefined,
          region: typeof parsed.region === "string" ? parsed.region : undefined,
          country: typeof parsed.country === "string" ? parsed.country : undefined,
          postalCode: typeof parsed.postalCode === "string" ? parsed.postalCode : undefined,
          source: typeof parsed.source === "string" ? parsed.source : undefined,
          capturedAt: typeof parsed.capturedAt === "string" ? parsed.capturedAt : new Date().toISOString(),
        };
      }
    } catch {
      // ignore
    }
  }

  // Also check cookie if header is not present
  if (!location) {
    try {
      let rawCookie: string | undefined;
      if ("cookies" in req && typeof (req as any).cookies?.get === "function") {
        rawCookie = (req as any).cookies.get("admin_loc")?.value;
      } else {
        const cookieHeader = headers.get("cookie") || "";
        const match = cookieHeader.match(/(?:^|;\s*)admin_loc=([^;]+)/);
        if (match) rawCookie = match[1];
      }
      if (rawCookie) {
        const parsed = JSON.parse(decodeURIComponent(rawCookie));
        if (parsed && typeof parsed === "object") {
          location = {
            status:
              parsed.status === "available" || parsed.status === "denied" || parsed.status === "unavailable"
                ? parsed.status
                : "unavailable",
            latitude: typeof parsed.latitude === "number" ? parsed.latitude : undefined,
            longitude: typeof parsed.longitude === "number" ? parsed.longitude : undefined,
            address: typeof parsed.address === "string" ? parsed.address : undefined,
            locality: typeof parsed.locality === "string" ? parsed.locality : undefined,
            city: typeof parsed.city === "string" ? parsed.city : undefined,
            region: typeof parsed.region === "string" ? parsed.region : undefined,
            country: typeof parsed.country === "string" ? parsed.country : undefined,
            postalCode: typeof parsed.postalCode === "string" ? parsed.postalCode : undefined,
            source: typeof parsed.source === "string" ? parsed.source : undefined,
            capturedAt: typeof parsed.capturedAt === "string" ? parsed.capturedAt : new Date().toISOString(),
          };
        }
      }
    } catch {
      // ignore
    }
  }

  return { ip, userAgent, location };
}
