import {
  BaseExecutor,
  mergeAbortSignals,
  mergeUpstreamExtraHeaders,
  type ExecuteInput,
} from "./base.ts";
import { FETCH_TIMEOUT_MS } from "../config/constants.ts";
import { normalizeSessionCookieHeader } from "@/lib/providers/webCookieAuth";
import { prepareToolMessages, buildToolAwareResult } from "../translator/webTools.ts";

const BLACKBOX_CHAT_API = "https://app.blackbox.ai/api/chat";
const BLACKBOX_DEFAULT_COOKIE = "next-auth.session-token";
const BLACKBOX_USER_AGENT =
  "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36";

const SESSION_CACHE_TTL_MS = 5 * 60_000; // 5 minutes

/**
 * Resolve the `validated` token for Blackbox `/api/chat` requests.
 *
 * Blackbox's web frontend ships a real validation token (exported as `tk` from
 * its Next.js JavaScript chunks). If the value sent in `transformedBody.validated`
 * does not match that token, the upstream returns HTTP 403 even when the session
 * cookie and subscription are valid — see issue #2252.
 *
 * Resolution priority:
 *   1. `BLACKBOX_WEB_VALIDATED_TOKEN` env var (operator-supplied, preferred)
 *   2. Random UUID fallback (the original behavior; works only as long as
 *      Blackbox doesn't enforce a specific frontend `tk` value)
 *
 * We do NOT scrape Blackbox's Next.js chunks at runtime to extract `tk` — that
 * coupling to their bundle hash is fragile and would silently break on every
 * frontend deploy. The env-var override gives operators who have figured out
 * the token a stable way to use it without patching code.
 */
export function resolveBlackboxValidatedToken(): string {
  const explicit = (process.env.BLACKBOX_WEB_VALIDATED_TOKEN || "").trim();
  if (explicit) return explicit;
  return crypto.randomUUID();
}

/**
 * Detect whether a Blackbox 403 response body indicates that the `validated`
 * token is the problem (as opposed to a missing cookie or expired subscription).
 * Surfaces the BLACKBOX_WEB_VALIDATED_TOKEN env var as the next step.
 */
function isBlackboxValidatedTokenError(responseText: string): boolean {
  const lower = (responseText || "").toLowerCase();
  return (
    lower.includes("invalid validated token") ||
    lower.includes("invalid validated") ||
    lower.includes("validation token") ||
    lower.includes("invalid token")
  );
}

type CachedSession = {
  sessionData: Record<string, unknown> | null;
  subscriptionCache: Record<string, unknown> | null;
  teamAccount: string;
  fetchedAt: number;
};

const MAX_SESSIONS = 100;
const sessionCache = new Map<string, CachedSession>();

type BlackboxMessage = {
  id: string;
  role: "user" | "assistant";
  content: string;
};

function extractMessageText(content: unknown): string {
  if (typeof content === "string") {
    return content.trim();
  }

  if (!Array.isArray(content)) {
    return "";
  }

  return content
    .map((part) => {
      if (!part || typeof part !== "object") return "";
      const item = part as Record<string, unknown>;
      if (item.type === "text" && typeof item.text === "string") {
        return item.text;
      }
      if (item.type === "input_text" && typeof item.text === "string") {
        return item.text;
      }
      return "";
    })
    .filter((part) => part.trim().length > 0)
    .join("\n")
    .trim();
}

function parseOpenAIMessages(
  messages: Array<Record<string, unknown>>,
  chatId: string
): BlackboxMessage[] {
  const systemParts: string[] = [];
  const parsed: BlackboxMessage[] = [];

  for (const message of messages) {
    const role = String(message.role || "user");
    const content = extractMessageText(message.content);
    if (!content) continue;

    if (role === "system" || role === "developer") {
      systemParts.push(content);
      continue;
    }

    if (role === "assistant" || role === "user") {
      parsed.push({
        id: role === "user" ? chatId : crypto.randomUUID(),
        role,
        content,
      });
    }
  }

  if (systemParts.length > 0) {
    const prefix = `System instructions:\n${systemParts.join("\n\n")}`;
    const firstUserIndex = parsed.findIndex((message) => message.role === "user");
    if (firstUserIndex >= 0) {
      parsed[firstUserIndex] = {
        ...parsed[firstUserIndex],
        content: `${prefix}\n\n${parsed[firstUserIndex].content}`,
      };
    } else {
      parsed.unshift({
        id: chatId,
        role: "user",
        content: prefix,
      });
    }
  }

  return parsed;
}

function estimateTokens(text: string): number {
  return Math.max(1, Math.ceil((text || "").length / 4));
}

function sseChunk(data: unknown): string {
  return `data: ${JSON.stringify(data)}\n\n`;
}

async function readTextResponse(
  body: ReadableStream<Uint8Array>,
  signal?: AbortSignal | null
): Promise<string> {
  const reader = body.getReader();
  const decoder = new TextDecoder();
  let text = "";

  try {
    while (true) {
      if (signal?.aborted) {
        throw signal.reason ?? new DOMException("Aborted", "AbortError");
      }

      const { value, done } = await reader.read();
      if (done) break;
      text += decoder.decode(value, { stream: true });
    }

    text += decoder.decode();
    return text;
  } finally {
    reader.releaseLock();
  }
}

function buildStreamingResponse(
  responseText: string,
  model: string,
  id: string,
  created: number
): ReadableStream<Uint8Array> {
  const encoder = new TextEncoder();

  return new ReadableStream(
    {
      start(controller) {
        controller.enqueue(
          encoder.encode(
            sseChunk({
              id,
              object: "chat.completion.chunk",
              created,
              model,
              system_fingerprint: null,
              choices: [
                {
                  index: 0,
                  delta: { role: "assistant" },
                  finish_reason: null,
                  logprobs: null,
                },
              ],
            })
          )
        );

        if (responseText) {
          controller.enqueue(
            encoder.encode(
              sseChunk({
                id,
                object: "chat.completion.chunk",
                created,
                model,
                system_fingerprint: null,
                choices: [
                  {
                    index: 0,
                    delta: { content: responseText },
                    finish_reason: null,
                    logprobs: null,
                  },
                ],
              })
            )
          );
        }

        controller.enqueue(
          encoder.encode(
            sseChunk({
              id,
              object: "chat.completion.chunk",
              created,
              model,
              system_fingerprint: null,
              choices: [{ index: 0, delta: {}, finish_reason: "stop", logprobs: null }],
            })
          )
        );
        controller.enqueue(encoder.encode("data: [DONE]\n\n"));
        controller.close();
      },
    },
    { highWaterMark: 16384 }
  );
}

function buildNonStreamingResponse(
  responseText: string,
  model: string,
  id: string,
  created: number
) {
  const completionTokens = estimateTokens(responseText);

  return new Response(
    JSON.stringify({
      id,
      object: "chat.completion",
      created,
      model,
      system_fingerprint: null,
      choices: [
        {
          index: 0,
          message: { role: "assistant", content: responseText },
          finish_reason: "stop",
          logprobs: null,
        },
      ],
      usage: {
        prompt_tokens: completionTokens,
        completion_tokens: completionTokens,
        total_tokens: completionTokens * 2,
      },
    }),
    {
      status: 200,
      headers: { "Content-Type": "application/json" },
    }
  );
}

export function normalizeBlackboxCookieHeader(apiKey: string): string {
  return normalizeSessionCookieHeader(apiKey, BLACKBOX_DEFAULT_COOKIE);
}

export class BlackboxWebExecutor extends BaseExecutor {
  constructor() {
    super("blackbox-web", { id: "blackbox-web", baseUrl: BLACKBOX_CHAT_API });
  }

  async execute({
    model,
    body,
    stream,
    credentials,
    signal,
    log,
    upstreamExtraHeaders,
  }: ExecuteInput) {
    const bodyObj = (body || {}) as Record<string, unknown>;
    const messages = bodyObj.messages as
      | Array<Record<string, unknown>>
      | undefined;
    if (!messages || !Array.isArray(messages) || messages.length === 0) {
      const errorResponse = new Response(
        JSON.stringify({
          error: {
            message: "Missing or empty messages array",
            type: "invalid_request",
          },
        }),
        { status: 400, headers: { "Content-Type": "application/json" } }
      );
      return {
        response: errorResponse,
        url: BLACKBOX_CHAT_API,
        headers: {},
        transformedBody: body,
      };
    }

    const { hasTools, requestedTools, effectiveMessages } = prepareToolMessages(bodyObj, messages as Array<{ role: string; content: unknown }>);
    const chatId = crypto.randomUUID().slice(0, 7);
    const parsedMessages = parseOpenAIMessages(effectiveMessages, chatId);
    if (parsedMessages.length === 0) {
      const errorResponse = new Response(
        JSON.stringify({
          error: {
            message: "Empty query after processing messages",
            type: "invalid_request",
          },
        }),
        { status: 400, headers: { "Content-Type": "application/json" } }
      );
      return {
        response: errorResponse,
        url: BLACKBOX_CHAT_API,
        headers: {},
        transformedBody: body,
      };
    }

    const cookieHeader = normalizeBlackboxCookieHeader(credentials.apiKey || "");
    const baseHeaders: Record<string, string> = {
      Accept: "application/json",
      Cookie: cookieHeader,
      Origin: "https://app.blackbox.ai",
      "User-Agent": BLACKBOX_USER_AGENT,
    };

    // Fetch session + subscription — Blackbox requires these in the request body.
    // Cached per cookie to avoid redundant round-trips on every request.
    let sessionData: Record<string, unknown> | null = null;
    let subscriptionCache: Record<string, unknown> | null = null;
    let teamAccount = "";

    const cacheKey = cookieHeader;
    const cached = sessionCache.get(cacheKey);
    if (cached && Date.now() - cached.fetchedAt < SESSION_CACHE_TTL_MS) {
      sessionData = cached.sessionData;
      subscriptionCache = cached.subscriptionCache;
      teamAccount = cached.teamAccount;
      log?.debug?.("BLACKBOX-WEB", `Session cache hit (${teamAccount || "no email"})`);
    } else {
      const sideSignal = signal
        ? mergeAbortSignals(signal, AbortSignal.timeout(10_000))
        : AbortSignal.timeout(10_000);

      try {
        const sessionRes = await fetch("https://app.blackbox.ai/api/auth/session", {
          method: "GET",
          headers: { ...baseHeaders, Accept: "application/json" },
          signal: sideSignal,
        });
        sessionData = sessionRes.ok ? ((await sessionRes.json()) as Record<string, unknown>) : null;
        const email = (sessionData as any)?.user?.email as string | undefined;
        teamAccount = email || "";
        log?.debug?.("BLACKBOX-WEB", `Session email: ${email ?? "none"}`);

        if (email) {
          const subRes = await fetch("https://app.blackbox.ai/api/check-subscription", {
            method: "POST",
            headers: { ...baseHeaders, "Content-Type": "application/json" },
            body: JSON.stringify({ email }),
            signal: sideSignal,
          });
          const rawSub = subRes.ok ? ((await subRes.json()) as Record<string, unknown>) : null;
          if (rawSub) {
            subscriptionCache = {
              status: rawSub.hasActiveSubscription ? "PREMIUM" : "FREE",
              customerId: rawSub.customerId ?? null,
              expiryTimestamp: rawSub.expiryTimestamp ?? null,
              lastChecked: Date.now(),
              isTrialSubscription: rawSub.isTrialSubscription ?? false,
              hasPaymentVerificationFailure: false,
              verificationFailureTimestamp: null,
              requiresAuthentication: false,
              isTeam: rawSub.isTeam ?? false,
              numSeats: rawSub.numSeats ?? 1,
              provider: rawSub.provider ?? null,
              previouslySubscribed: rawSub.previouslySubscribed ?? false,
              activeInsuffientCredits: rawSub.activeInsuffientCredits ?? false,
            };
            log?.debug?.("BLACKBOX-WEB", `Subscription: ${subscriptionCache.status}`);
          }
        }

        sessionCache.set(cacheKey, {
          sessionData,
          subscriptionCache,
          teamAccount,
          fetchedAt: Date.now(),
        });
        while (sessionCache.size > MAX_SESSIONS) {
          const oldestKey = sessionCache.keys().next().value;
          if (oldestKey !== undefined) sessionCache.delete(oldestKey);
          else break;
        }
      } catch (diagErr) {
        log?.debug?.("BLACKBOX-WEB", `Session/subscription fetch failed (non-fatal): ${diagErr}`);
      }
    }

    const headers: Record<string, string> = {
      ...baseHeaders,
      Accept: "text/plain, */*",
      "Content-Type": "application/json",
      Referer: `https://app.blackbox.ai/chat/${chatId}`,
    };
    mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);

    const transformedBody = {
      messages: parsedMessages,
      id: chatId,
      previewToken: null,
      userId: credentials.providerSpecificData?.userId ?? null,
      codeModelMode: true,
      trendingAgentMode: {},
      isMicMode: false,
      userSystemPrompt: null,
      maxTokens: Number((body as Record<string, unknown>).max_tokens) || 1024,
      playgroundTopP: null,
      playgroundTemperature: null,
      isChromeExt: false,
      githubToken: "",
      clickedAnswer2: false,
      clickedAnswer3: false,
      clickedForceWebSearch: false,
      visitFromDelta: false,
      isMemoryEnabled: false,
      mobileClient: false,
      userSelectedModel: model || null,
      userSelectedAgent: "VscodeAgent",
      // Issue #2252: prefer operator-supplied BLACKBOX_WEB_VALIDATED_TOKEN over
      // a random UUID — Blackbox's `/api/chat` rejects mismatched tokens with 403.
      validated: resolveBlackboxValidatedToken(),
      imageGenerationMode: false,
      imageGenMode: "autoMode",
      webSearchModePrompt: false,
      deepSearchMode: false,
      promptSelection: "",
      domains: null,
      vscodeClient: false,
      codeInterpreterMode: false,
      customProfile: {
        name: "",
        occupation: "",
        traits: [],
        additionalInfo: "",
        enableNewChats: false,
      },
      webSearchModeOption: {
        autoMode: true,
        webMode: false,
        offlineMode: false,
      },
      session: sessionData,
      isPremium: subscriptionCache
        ? subscriptionCache.status === "PREMIUM"
        : (credentials.providerSpecificData?.isPremium ?? true),
      teamAccount,
      subscriptionCache,
      beastMode: false,
      reasoningMode: false,
      designerMode: false,
      workspaceId: "",
      asyncMode: false,
      integrations: {},
      isTaskPersistent: false,
      selectedElement: null,
    };

    const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
    const combinedSignal = signal ? mergeAbortSignals(signal, timeoutSignal) : timeoutSignal;

    let upstreamResponse: Response;
    try {
      upstreamResponse = await fetch(BLACKBOX_CHAT_API, {
        method: "POST",
        headers,
        body: JSON.stringify(transformedBody),
        signal: combinedSignal,
      });
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      log?.error?.("BLACKBOX-WEB", `Fetch failed: ${message}`);
      const errorResponse = new Response(
        JSON.stringify({
          error: {
            message: `Blackbox Web connection failed: ${message}`,
            type: "upstream_error",
          },
        }),
        { status: 502, headers: { "Content-Type": "application/json" } }
      );
      return {
        response: errorResponse,
        url: BLACKBOX_CHAT_API,
        headers,
        transformedBody,
      };
    }

    if (!upstreamResponse.ok) {
      const status = upstreamResponse.status;
      let message = `Blackbox Web returned HTTP ${status}`;
      // Issue #2252: distinguish "wrong validated token" from "expired cookie"
      // when 403 carries a token-specific body — the fix is different in each case.
      const errorBody = await upstreamResponse.text().catch(() => "");
      if (status === 403 && isBlackboxValidatedTokenError(errorBody)) {
        message =
          "Blackbox Web rejected the request with an invalid `validated` token. " +
          "If you have a valid frontend token (the `tk` value from app.blackbox.ai's " +
          "Next.js bundle), set BLACKBOX_WEB_VALIDATED_TOKEN in your environment and restart.";
      } else if (status === 401 || status === 403) {
        message =
          "Blackbox Web auth failed — your app.blackbox.ai session cookie may be missing or expired.";
      } else if (status === 429) {
        message = "Blackbox Web rate limited the session. Wait a moment and retry.";
      }
      const errorResponse = new Response(
        JSON.stringify({
          error: {
            message,
            type: "upstream_error",
            code: `HTTP_${status}`,
          },
        }),
        { status, headers: { "Content-Type": "application/json" } }
      );
      return {
        response: errorResponse,
        url: BLACKBOX_CHAT_API,
        headers,
        transformedBody,
      };
    }

    if (!upstreamResponse.body) {
      const errorResponse = new Response(
        JSON.stringify({
          error: {
            message: "Blackbox Web returned an empty response body",
            type: "upstream_error",
          },
        }),
        { status: 502, headers: { "Content-Type": "application/json" } }
      );
      return {
        response: errorResponse,
        url: BLACKBOX_CHAT_API,
        headers,
        transformedBody,
      };
    }

    const responseText = (await readTextResponse(upstreamResponse.body, signal)).trim();

    log?.debug?.("BLACKBOX-WEB", `Response (first 300 chars): ${responseText.slice(0, 300)}`);
    log?.debug?.("BLACKBOX-WEB", `userSelectedModel sent: ${transformedBody.userSelectedModel}`);
    log?.debug?.("BLACKBOX-WEB", `isPremium sent: ${transformedBody.isPremium}`);

    // Blackbox sometimes returns HTTP 200 with in-band error messages instead of proper HTTP status codes.
    // Detect known error patterns and surface them as real errors.
    const lowerText = responseText.toLowerCase();
    const isSubscriptionError =
      /not upgraded|upgrade to a premium plan|upgrade.required/i.test(responseText) ||
      lowerText.includes("please upgrade");
    const isAuthError =
      /please login|login required|authentication required/i.test(responseText) &&
      !isSubscriptionError;
    const isRateLimit = /rate limit|too many requests/i.test(responseText) && !isSubscriptionError;

    if (isSubscriptionError) {
      log?.warn?.("BLACKBOX-WEB", "Blackbox returned subscription error in response body");
      const errorResponse = new Response(
        JSON.stringify({
          error: {
            message:
              "Blackbox reports your account lacks a premium subscription. " +
              "If you have a paid plan, re-paste your session cookie from app.blackbox.ai.",
            type: "upstream_error",
            code: "BLACKBOX_SUBSCRIPTION_REQUIRED",
          },
        }),
        { status: 402, headers: { "Content-Type": "application/json" } }
      );
      return { response: errorResponse, url: BLACKBOX_CHAT_API, headers, transformedBody };
    }

    if (isAuthError) {
      log?.warn?.("BLACKBOX-WEB", "Blackbox returned auth error in response body");
      const errorResponse = new Response(
        JSON.stringify({
          error: {
            message:
              "Blackbox session is not authenticated — re-paste next-auth.session-token from app.blackbox.ai",
            type: "upstream_error",
            code: "BLACKBOX_AUTH_REQUIRED",
          },
        }),
        { status: 401, headers: { "Content-Type": "application/json" } }
      );
      return { response: errorResponse, url: BLACKBOX_CHAT_API, headers, transformedBody };
    }

    if (isRateLimit) {
      log?.warn?.("BLACKBOX-WEB", "Blackbox returned rate-limit error in response body");
      const errorResponse = new Response(
        JSON.stringify({
          error: {
            message: "Blackbox Web rate limited the session. Wait a moment and retry.",
            type: "upstream_error",
            code: "BLACKBOX_RATE_LIMIT",
          },
        }),
        { status: 429, headers: { "Content-Type": "application/json" } }
      );
      return { response: errorResponse, url: BLACKBOX_CHAT_API, headers, transformedBody };
    }

    const id = `chatcmpl-blackbox-${crypto.randomUUID().slice(0, 12)}`;
    const created = Math.floor(Date.now() / 1000);

    if (hasTools) {
      const { content, toolCalls, finishReason } = buildToolAwareResult(responseText, requestedTools, "bbx");
      if (toolCalls) {
        const toolResponse = new Response(
          JSON.stringify({
            id, object: "chat.completion", created, model,
            choices: [{ index: 0, message: { role: "assistant", content: null, tool_calls: toolCalls }, finish_reason: finishReason }],
          }),
          { status: 200, headers: { "Content-Type": "application/json" } }
        );
        return { response: toolResponse, url: BLACKBOX_CHAT_API, headers, transformedBody };
      }
      const finalResponse = stream
        ? new Response(buildStreamingResponse(content, model, id, created), {
            status: 200,
            headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "X-Accel-Buffering": "no" },
          })
        : buildNonStreamingResponse(content, model, id, created);
      return { response: finalResponse, url: BLACKBOX_CHAT_API, headers, transformedBody };
    }

    const finalResponse = stream
      ? new Response(buildStreamingResponse(responseText, model, id, created), {
          status: 200,
          headers: {
            "Content-Type": "text/event-stream",
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no",
          },
        })
      : buildNonStreamingResponse(responseText, model, id, created);

    return {
      response: finalResponse,
      url: BLACKBOX_CHAT_API,
      headers,
      transformedBody,
    };
  }
}
