/**
 * API Token authentication for Finzone v2 public REST API.
 *
 * Token format: fz_<env>_<24-rand>  (env = live | test).
 * Stored: prefix = first 11 chars (e.g. "fz_live_abc"), hash = bcrypt(full token).
 *
 * Usage in route handlers:
 *   const ctx = await requireApiToken(req, ["post:write"]);
 *   if (ctx instanceof Response) return ctx;
 *   // ctx.user, ctx.tokenPrefix, ctx.audit(...)
 */
import * as bcrypt from "bcryptjs";
import { NextRequest } from "next/server";
import { randomBytes } from "node:crypto";
import { prisma } from "./db";

export type Scope =
  | "post:read"
  | "post:write"
  | "post:publish"
  | "post:delete"
  | "media:upload"
  | "category:read"
  | "category:write"
  | "seo:audit";

export const ALL_SCOPES: Scope[] = [
  "post:read",
  "post:write",
  "post:publish",
  "post:delete",
  "media:upload",
  "category:read",
  "category:write",
  "seo:audit",
];

const PREFIX_LEN = 11; // "fz_live_xxx"

export type ApiAuthContext = {
  tokenId: string;
  tokenPrefix: string;
  userId: string;
  user: { id: string; email: string; name: string; role: string };
  scopes: Scope[];
  ip: string | null;
  audit: (params: {
    action: string;
    target?: string | null;
    meta?: Record<string, unknown>;
  }) => Promise<void>;
};

export function generateToken(env: "live" | "test" = "live"): {
  full: string;
  prefix: string;
} {
  const rand = randomBytes(18).toString("base64url"); // 24 chars
  const full = `fz_${env}_${rand}`;
  const prefix = full.slice(0, PREFIX_LEN);
  return { full, prefix };
}

export async function hashToken(token: string): Promise<string> {
  return bcrypt.hash(token, 10);
}

export function getClientIp(req: NextRequest): string | null {
  const xff = req.headers.get("x-forwarded-for");
  if (xff) return xff.split(",")[0]?.trim() || null;
  return req.headers.get("x-real-ip") ?? null;
}

function ipMatches(ip: string | null, allow: string[]): boolean {
  if (allow.length === 0) return true;
  if (!ip) return false;
  return allow.some((rule) => ip === rule || ip.startsWith(rule));
}

function jsonError(status: number, code: string, message: string): Response {
  return new Response(JSON.stringify({ error: { code, message } }), {
    status,
    headers: { "content-type": "application/json" },
  });
}

export async function requireApiToken(
  req: NextRequest,
  required: Scope[],
): Promise<ApiAuthContext | Response> {
  const authHeader = req.headers.get("authorization");
  if (!authHeader || !authHeader.toLowerCase().startsWith("bearer ")) {
    return jsonError(
      401,
      "missing_token",
      "Cần header Authorization: Bearer <token>.",
    );
  }
  const token = authHeader.slice(7).trim();
  if (token.length < PREFIX_LEN + 8) {
    return jsonError(401, "invalid_token", "Token không hợp lệ.");
  }
  const prefix = token.slice(0, PREFIX_LEN);

  const row = await prisma.apiToken.findUnique({
    where: { prefix },
    include: {
      user: { select: { id: true, email: true, name: true, role: true } },
    },
  });
  if (!row || row.revoked) {
    return jsonError(401, "invalid_token", "Token sai hoặc đã thu hồi.");
  }
  if (row.expiresAt && row.expiresAt < new Date()) {
    return jsonError(401, "expired_token", "Token đã hết hạn.");
  }

  const ok = await bcrypt.compare(token, row.hash);
  if (!ok) return jsonError(401, "invalid_token", "Token không khớp.");

  const ip = getClientIp(req);
  if (!ipMatches(ip, row.ipAllowlist)) {
    return jsonError(
      403,
      "ip_not_allowed",
      `IP ${ip ?? "?"} không trong allowlist.`,
    );
  }

  const scopes = row.scopes as Scope[];
  const missing = required.filter((s) => !scopes.includes(s));
  if (missing.length > 0) {
    return jsonError(
      403,
      "insufficient_scope",
      `Token thiếu scope: ${missing.join(", ")}`,
    );
  }

  // touch lastUsedAt — fire and forget
  prisma.apiToken
    .update({ where: { id: row.id }, data: { lastUsedAt: new Date() } })
    .catch(() => {});

  const ctx: ApiAuthContext = {
    tokenId: row.id,
    tokenPrefix: row.prefix,
    userId: row.createdBy,
    user: row.user,
    scopes,
    ip,
    audit: async ({ action, target, meta }) => {
      await prisma.auditLog.create({
        data: {
          actorId: row.createdBy,
          actorName: row.user.name,
          actorEmail: row.user.email,
          source: "api",
          tokenPrefix: row.prefix,
          ipAddress: ip,
          action,
          target: target ?? null,
          meta: (meta ?? null) as never,
        },
      });
    },
  };
  return ctx;
}

export function apiError(
  status: number,
  code: string,
  message: string,
  details?: unknown,
): Response {
  return new Response(
    JSON.stringify({ error: { code, message, ...(details ? { details } : {}) } }),
    { status, headers: { "content-type": "application/json" } },
  );
}

export function apiOk(data: unknown, status = 200): Response {
  return new Response(JSON.stringify(data), {
    status,
    headers: { "content-type": "application/json" },
  });
}
