import {
  constantTimeEqual,
  createCaptchaEngine,
  MemoryCaptchaStore,
  randomId,
  sha256,
  type CaptchaEngine,
  type CreateCaptchaOptions,
  type VerifyCaptchaInput,
} from "@wrnexus/captcha/server";
import { MemoryManagedProjectStore } from "./store.ts";
import type {
  ManagedCaptchaProject,
  ManagedCaptchaServiceOptions,
  ManagedProjectStore,
  PublicProjectKey,
} from "./types.ts";

function json(body: unknown, status = 200, headers: HeadersInit = {}): Response {
  return Response.json(body, {
    status,
    headers: { "cache-control": "no-store", "x-content-type-options": "nosniff", ...headers },
  });
}
function corsHeaders(request: Request): HeadersInit {
  const origin = request.headers.get("origin");
  return origin ? { "access-control-allow-origin": origin, vary: "Origin" } : {};
}
function month(now: number): string {
  return new Date(now).toISOString().slice(0, 7);
}
function bearer(request: Request): string {
  return request.headers.get("authorization")?.replace(/^Bearer\s+/i, "") ?? "";
}
function hostnameFrom(request: Request, payload: Record<string, unknown>): string {
  const origin = request.headers.get("origin");
  if (origin) return new URL(origin).hostname.toLowerCase();
  const explicit =
    typeof payload.hostname === "string" ? payload.hostname.trim().toLowerCase() : "";
  return explicit || new URL(request.url).hostname.toLowerCase();
}
function publicProject(
  project: ManagedCaptchaProject,
): Omit<ManagedCaptchaProject, "secretHash" | "engineSecret"> {
  const { secretHash: _secretHash, engineSecret: _engineSecret, ...safe } = project;
  return safe;
}

export class ManagedCaptchaService {
  readonly projects: ManagedProjectStore;
  private readonly engines = new Map<string, CaptchaEngine>();
  private readonly challengeOwners = new Map<string, string>();
  private readonly now: () => number;
  private readonly randomBytes: (length: number) => Uint8Array;
  private readonly defaultMonthlyQuota: number;

  constructor(private readonly options: ManagedCaptchaServiceOptions) {
    if (!options.adminToken || options.adminToken.length < 24)
      throw new TypeError("A strong admin token is required");
    this.projects = options.projectStore ?? new MemoryManagedProjectStore();
    this.now = options.now ?? Date.now;
    this.randomBytes =
      options.randomBytes ?? ((length) => crypto.getRandomValues(new Uint8Array(length)));
    this.defaultMonthlyQuota = options.defaultMonthlyQuota ?? 10_000;
  }

  async createProject(input: {
    name: string;
    allowedHostnames?: string[];
    monthlyQuota?: number;
  }): Promise<PublicProjectKey> {
    const timestamp = this.now();
    const secretKey = `wrn_secret_${randomId(this.randomBytes, 32)}`;
    const project: ManagedCaptchaProject = {
      id: `project_${randomId(this.randomBytes, 16)}`,
      name: input.name.trim(),
      siteKey: `wrn_site_${randomId(this.randomBytes, 20)}`,
      secretHash: await sha256(secretKey),
      engineSecret: randomId(this.randomBytes, 48),
      allowedHostnames: [
        ...new Set(
          (input.allowedHostnames ?? [])
            .map((hostname) => hostname.trim().toLowerCase())
            .filter(Boolean),
        ),
      ],
      monthlyQuota: input.monthlyQuota ?? this.defaultMonthlyQuota,
      usageMonth: month(timestamp),
      usageCount: 0,
      enabled: true,
      createdAt: timestamp,
      updatedAt: timestamp,
    };
    if (!project.name) throw new TypeError("Project name is required");
    await this.projects.create(project);
    return {
      id: project.id,
      name: project.name,
      siteKey: project.siteKey,
      secretKey,
      allowedHostnames: project.allowedHostnames,
      monthlyQuota: project.monthlyQuota,
    };
  }

  async rotateSecret(id: string): Promise<{ secretKey: string }> {
    const project = await this.projects.getById(id);
    if (!project) throw new Error("Unknown project");
    const secretKey = `wrn_secret_${randomId(this.randomBytes, 32)}`;
    project.secretHash = await sha256(secretKey);
    project.updatedAt = this.now();
    await this.projects.update(project);
    return { secretKey };
  }

  async handle(request: Request): Promise<Response> {
    try {
      return await this.route(request);
    } catch {
      return json({ error: "invalid-request" }, 400, corsHeaders(request));
    }
  }

  private async route(request: Request): Promise<Response> {
    const url = new URL(request.url);
    if (request.method === "OPTIONS") {
      return new Response(null, {
        status: 204,
        headers: {
          ...corsHeaders(request),
          "access-control-allow-methods": "GET,POST,OPTIONS",
          "access-control-allow-headers": "content-type,authorization",
          "access-control-max-age": "600",
        },
      });
    }
    if (request.method === "POST" && url.pathname === "/v1/challenges")
      return this.createChallenge(request);
    if (request.method === "POST" && url.pathname === "/v1/solve")
      return this.solveChallenge(request);
    if (request.method === "POST" && url.pathname === "/v1/verify")
      return this.verifyToken(request);
    const audio = url.pathname.match(/^\/v1\/audio\/([^/]+)$/);
    if (audio && (request.method === "GET" || request.method === "HEAD"))
      return this.audioChallenge(request, decodeURIComponent(audio[1]!));
    if (url.pathname === "/v1/projects" && request.method === "POST")
      return this.adminCreateProject(request);
    if (url.pathname === "/v1/projects" && request.method === "GET")
      return this.adminListProjects(request);
    const rotate = url.pathname.match(/^\/v1\/projects\/([^/]+)\/rotate-secret$/);
    if (rotate && request.method === "POST")
      return this.adminRotate(request, decodeURIComponent(rotate[1]!));
    return json({ error: "not-found" }, 404);
  }

  private engine(project: ManagedCaptchaProject): CaptchaEngine {
    let engine = this.engines.get(project.id);
    if (!engine) {
      engine =
        this.options.engineFactory?.(project) ??
        createCaptchaEngine({
          secret: project.engineSecret,
          store: this.options.captchaStore ?? new MemoryCaptchaStore(),
          basePath: "/v1",
          bindIp: false,
        });
      this.engines.set(project.id, engine);
    }
    return engine;
  }

  private async createChallenge(request: Request): Promise<Response> {
    const payload = (await request.json()) as Record<string, unknown>;
    const project = await this.projects.getBySiteKey(String(payload.siteKey ?? ""));
    if (!project || !project.enabled)
      return json({ error: "invalid-site-key" }, 403, corsHeaders(request));
    const hostname = hostnameFrom(request, payload);
    if (project.allowedHostnames.length && !project.allowedHostnames.includes(hostname))
      return json({ error: "hostname-not-allowed" }, 403, corsHeaders(request));
    if (!(await this.consumeQuota(project)))
      return json({ error: "quota-exceeded" }, 429, corsHeaders(request));
    const challenge = await this.engine(project).create({
      ...(payload as unknown as CreateCaptchaOptions),
      hostname,
    });
    const publicBase = this.options.baseUrl?.replace(/\/+$/, "") ?? "";
    challenge.provider = "wrnexus-managed";
    challenge.verifyUrl = `${publicBase}/v1/solve`;
    if (challenge.audioUrl && publicBase) challenge.audioUrl = `${publicBase}${challenge.audioUrl}`;
    this.challengeOwners.set(challenge.id, project.id);
    return json(challenge, 201, corsHeaders(request));
  }

  private async audioChallenge(request: Request, id: string): Promise<Response> {
    const owner = this.challengeOwners.get(id);
    if (!owner) return new Response("Not Found", { status: 404, headers: corsHeaders(request) });
    const project = await this.projects.getById(owner);
    if (!project) return new Response("Not Found", { status: 404, headers: corsHeaders(request) });
    const key = new URL(request.url).searchParams.get("key") ?? "";
    try {
      const audio = await this.engine(project).renderAudio(id, key);
      if (!audio) return new Response("Not Found", { status: 404, headers: corsHeaders(request) });

      const body: BodyInit | null =
        request.method === "HEAD" ? null : Uint8Array.from(audio.bytes).buffer;

      return new Response(body, {
        headers: {
          ...corsHeaders(request),
          "content-type": audio.contentType,
          "content-length": String(audio.bytes.byteLength),
          "cache-control": "private, no-store, max-age=0",
          "x-content-type-options": "nosniff",
        },
      });
    } catch (error) {
      console.error(
        "[wrnexus/managed-captcha] Unable to render CAPTCHA audio:",
        error instanceof Error ? error.message : error,
      );
      return new Response("CAPTCHA audio is unavailable", {
        status: 503,
        headers: {
          ...corsHeaders(request),
          "content-type": "text/plain; charset=utf-8",
          "cache-control": "no-store, max-age=0",
          "x-content-type-options": "nosniff",
        },
      });
    }
  }

  private async solveChallenge(request: Request): Promise<Response> {
    const payload = (await request.json()) as Record<string, unknown>;
    const id = String(payload.challengeId ?? "");
    const owner = this.challengeOwners.get(id);
    if (!owner)
      return json(
        { success: false, code: "invalid-input", message: "Unknown challenge" },
        400,
        corsHeaders(request),
      );
    const project = await this.projects.getById(owner);
    if (!project) return json({ success: false, code: "invalid-input" }, 400, corsHeaders(request));
    const hostname = hostnameFrom(request, payload);
    const result = await this.engine(project).verify({
      ...(payload as unknown as VerifyCaptchaInput),
      hostname,
    });
    result.provider = "wrnexus-managed";
    if (result.success || result.code === "expired" || result.code === "attempts-exhausted")
      this.challengeOwners.delete(id);
    return json(result, result.success ? 200 : 400, corsHeaders(request));
  }

  private async verifyToken(request: Request): Promise<Response> {
    const secret = bearer(request);
    const project = secret ? await this.projects.getBySecretHash(await sha256(secret)) : undefined;
    if (!project || !project.enabled) return json({ success: false, code: "invalid-secret" }, 401);
    const payload = (await request.json()) as VerifyCaptchaInput;
    const result = await this.engine(project).verifyResponseToken(payload);
    result.provider = "wrnexus-managed";
    return json(result, result.success ? 200 : 400);
  }

  private async consumeQuota(project: ManagedCaptchaProject): Promise<boolean> {
    const currentMonth = month(this.now());
    if (project.usageMonth !== currentMonth) {
      project.usageMonth = currentMonth;
      project.usageCount = 0;
    }
    if (project.usageCount >= project.monthlyQuota) return false;
    project.usageCount += 1;
    project.updatedAt = this.now();
    await this.projects.update(project);
    return true;
  }

  private adminAllowed(request: Request): boolean {
    const supplied = bearer(request);
    return supplied.length > 0 && constantTimeEqual(supplied, this.options.adminToken);
  }
  private async adminCreateProject(request: Request): Promise<Response> {
    if (!this.adminAllowed(request)) return json({ error: "unauthorized" }, 401);
    try {
      return json(
        await this.createProject(
          (await request.json()) as {
            name: string;
            allowedHostnames?: string[];
            monthlyQuota?: number;
          },
        ),
        201,
      );
    } catch (error) {
      return json({ error: error instanceof Error ? error.message : "invalid-project" }, 400);
    }
  }
  private async adminListProjects(request: Request): Promise<Response> {
    if (!this.adminAllowed(request)) return json({ error: "unauthorized" }, 401);
    return json({ projects: (await this.projects.list()).map(publicProject) });
  }
  private async adminRotate(request: Request, id: string): Promise<Response> {
    if (!this.adminAllowed(request)) return json({ error: "unauthorized" }, 401);
    try {
      return json(await this.rotateSecret(id));
    } catch {
      return json({ error: "not-found" }, 404);
    }
  }
}
