import { db } from "@/app/db/db";
import { tasks } from "@/app/db/schema/tasks/tasks";
import { subTasks } from "@/app/db/schema/tasks/subtask";
import { and, desc, eq } from "drizzle-orm";
import {
  BULK_DOWNLOAD_TASK_SOURCE,
  BulkDownloadSelectionInput,
  BulkDownloadStatus,
  buildBulkDownloadRecord,
  buildSelectionConfig,
  parseDelivery,
} from "./model";

// The document-service entry point on the backend. It resolves the selection
// from the task's `inputs`, assembles the ZIP, emails the link (recipient from
// the JWT), and writes the result to the task's sub_task.outputs.
const BULK_DOWNLOAD_ENDPOINT = "/v1/documents/bulk-download";

type Dispatch = { ok: true } | { ok: false; status: number; message: string };

function isFailed(status: string | null): boolean {
  return status ? ["failed", "failure", "error"].includes(status.toLowerCase()) : false;
}

function defaultMessageForStatus(status: number): string {
  if (status === 409) return "A download for this selection has already been started.";
  if (status === 400) return "That selection can’t be downloaded — please adjust it and try again.";
  return "Couldn’t start the download — please try again.";
}

/** Pull FastAPI's error detail so a 400 (e.g. over the 500 cap) reaches the user verbatim. */
async function extractDetail(res: Response): Promise<string> {
  try {
    const body = (await res.json()) as Record<string, unknown>;
    const d = body.detail ?? body.message ?? body.error;
    if (typeof d === "string" && d.trim()) return d;
    if (Array.isArray(d) && typeof (d[0] as { msg?: unknown })?.msg === "string") {
      return (d[0] as { msg: string }).msg;
    }
  } catch {
    // not JSON — fall through to the status default
  }
  return defaultMessageForStatus(res.status);
}

async function dispatchBulkDownload(
  taskId: string,
  sessionToken: string | undefined,
): Promise<Dispatch> {
  const url = process.env.FASTAPI_API_URL;
  const key = process.env.FASTAPI_API_KEY;
  if (!url || !key) {
    console.error("FASTAPI_API_URL or FASTAPI_API_KEY not set");
    return { ok: false, status: 500, message: "Server misconfiguration" };
  }
  try {
    const res = await fetch(`${url}${BULK_DOWNLOAD_ENDPOINT}`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-api-key": key,
        // The recipient email is resolved from this JWT; no email in the body.
        Authorization: `Bearer ${sessionToken}`,
      },
      body: JSON.stringify({ task_id: taskId }),
    });
    if (res.ok) return { ok: true };
    return { ok: false, status: res.status, message: await extractDetail(res) };
  } catch (err) {
    console.error(`Failed to reach FastAPI ${BULK_DOWNLOAD_ENDPOINT}:`, err);
    return {
      ok: false,
      status: 502,
      message: "Couldn’t reach the document service — please try again.",
    };
  }
}

export type BulkDownloadTriggerOutcome =
  | { kind: "ok"; taskId: string }
  | { kind: "invalid_selection"; message: string }
  | { kind: "backend_error"; status: number; message: string };

/**
 * Record + dispatch a bulk document download (Modelling-run convention,
 * ADR-0008): one app-authored task whose `inputs` carries the selection
 * config, then the document service is handed only the task id — so a large
 * selection never travels in the HTTP body. A rejected dispatch marks the task
 * failed so it never lingers as a ghost.
 */
export async function triggerBulkDocumentDownload(args: {
  selection: BulkDownloadSelectionInput;
  sessionToken: string | undefined;
}): Promise<BulkDownloadTriggerOutcome> {
  const built = buildSelectionConfig(args.selection);
  if (!built.ok) return { kind: "invalid_selection", message: built.error };

  const record = buildBulkDownloadRecord({
    portfolioId: String(args.selection.portfolioId),
    config: built.config,
  });
  const now = new Date();
  const [task] = await db
    .insert(tasks)
    .values({ ...record, jobStarted: now })
    .returning();

  const dispatch = await dispatchBulkDownload(task.id, args.sessionToken);
  if (!dispatch.ok) {
    await db.update(tasks).set({ status: "failed" }).where(eq(tasks.id, task.id));
    return { kind: "backend_error", status: dispatch.status, message: dispatch.message };
  }

  await db.update(tasks).set({ status: "in progress" }).where(eq(tasks.id, task.id));
  return { kind: "ok", taskId: task.id };
}

function isComplete(status: string | null): boolean {
  return status
    ? ["completed", "complete", "success", "succeeded", "done"].includes(
        status.toLowerCase(),
      )
    : false;
}

/**
 * Status of a triggered download for in-app polling. The worker writes the
 * presigned URL to the sub_task's `outputs` on success; a selection that yields
 * zero documents fails the sub_task (no empty ZIP). Terminal state is taken from
 * completion/failure *status* (task or sub_task) as well as the parsed outputs —
 * so a finished job never hangs on "preparing" just because the link couldn't be
 * parsed. Portfolio-scoped so a task id can't be read against another portfolio.
 */
export async function getBulkDownloadStatus(
  portfolioId: string,
  taskId: string,
): Promise<BulkDownloadStatus | null> {
  const rows = await db
    .select({
      taskStatus: tasks.status,
      taskDone: tasks.jobCompleted,
      subStatus: subTasks.status,
      subDone: subTasks.jobCompleted,
      outputs: subTasks.outputs,
    })
    .from(tasks)
    .leftJoin(subTasks, eq(subTasks.taskId, tasks.id))
    .where(
      and(
        eq(tasks.id, taskId),
        eq(tasks.taskSource, BULK_DOWNLOAD_TASK_SOURCE),
        eq(tasks.sourceId, portfolioId),
      ),
    )
    .orderBy(desc(subTasks.updatedAt));

  if (rows.length === 0) return null;

  const delivery = rows.map((r) => parseDelivery(r.outputs)).find(Boolean) ?? null;
  const failed =
    isFailed(rows[0].taskStatus) || rows.some((r) => isFailed(r.subStatus));
  // A job whose sub_task (or task) reports completion is terminal even if the
  // outputs couldn't be parsed into a link — the link is still emailed.
  const complete =
    isComplete(rows[0].taskStatus) ||
    !!rows[0].taskDone ||
    rows.some((r) => isComplete(r.subStatus) || !!r.subDone);

  const state = delivery || complete ? "ready" : failed ? "failed" : "preparing";
  return { state, delivery };
}
