/**
 * Bulk-document-download domain model — pure logic for the Documents bulk
 * download. Mirrors the Modelling-run trigger convention (ADR-0008): one
 * app-authored `tasks` row whose `inputs` carries the selection config, then
 * the backend is handed only the task id.
 *
 * The backend resolves the union of (all properties in the given project
 * codes) and (the hand-picked landlord property ids), de-duplicated. Rule of
 * thumb: "include all" sends `project_codes` (so properties without a
 * landlord_property_id are still swept in); a ticked subset sends
 * `landlord_property_ids`. "All projects" enumerates the portfolio's project
 * codes — there is no server-side "whole portfolio" option by design.
 */

/** Backend cap; over this the trigger is rejected with a 400. */
export const MAX_BULK_DOWNLOAD_PROPERTIES = 500;

/**
 * The selection config, exactly as it is stored (JSON) in `tasks.inputs` and
 * read back by the worker. snake_case because the backend parses it directly —
 * do not add app-provenance keys here (the recipient email is resolved from
 * the JWT at dispatch, not carried in the config).
 */
export interface BulkDownloadConfig {
  /** HubSpot project codes (hubspot_deal_data.project_code). */
  project_codes?: string[];
  /** The hand-pick key (property.landlord_property_id). */
  landlord_property_ids?: string[];
  /** Names the ZIP only. */
  portfolio_id?: number;
}

/** Which projects the download is scoped to. */
export type BulkDownloadScope =
  | { kind: "project"; projectCode: string }
  | { kind: "all-projects"; projectCodes: string[] };

/** The UI's selection intent, resolved into a config server-side. */
export interface BulkDownloadSelectionInput {
  scope: BulkDownloadScope;
  /** true → "include all" (send project_codes); false → send the ticked ids. */
  includeAll: boolean;
  /** The ticked landlord_property_ids — used only when includeAll is false. */
  landlordPropertyIds: string[];
  portfolioId: number;
}

export type BuildConfigResult =
  | { ok: true; config: BulkDownloadConfig }
  | { ok: false; error: string };

/**
 * Map the UI selection onto the stored config:
 *   include all  → { project_codes, portfolio_id }
 *   ticked subset → { landlord_property_ids, portfolio_id }
 * At least one of project_codes / landlord_property_ids must be non-empty.
 */
export function buildSelectionConfig(
  input: BulkDownloadSelectionInput,
): BuildConfigResult {
  const portfolio_id = input.portfolioId;

  if (input.includeAll) {
    const rawCodes =
      input.scope.kind === "project"
        ? [input.scope.projectCode]
        : input.scope.projectCodes;
    const project_codes = [
      ...new Set(rawCodes.map((c) => c.trim()).filter((c) => c.length > 0)),
    ];
    if (project_codes.length === 0) {
      return { ok: false, error: "Pick at least one project to download." };
    }
    return { ok: true, config: { project_codes, portfolio_id } };
  }

  const landlord_property_ids = [
    ...new Set(input.landlordPropertyIds.map((i) => i.trim()).filter(Boolean)),
  ];
  if (landlord_property_ids.length === 0) {
    return {
      ok: false,
      error: "Tick at least one property, or choose “include all”.",
    };
  }
  if (landlord_property_ids.length > MAX_BULK_DOWNLOAD_PROPERTIES) {
    return {
      ok: false,
      error: `The selection has ${landlord_property_ids.length} properties, over the ${MAX_BULK_DOWNLOAD_PROPERTIES} limit — narrow your selection.`,
    };
  }
  return { ok: true, config: { landlord_property_ids, portfolio_id } };
}

/**
 * The marker the worker keys off, in `task_source` (ADR-0011). It receives only
 * the task id, re-reads this row, and guards on this value before assembling.
 */
export const BULK_DOWNLOAD_TASK_SOURCE = "app:bulk_document_download";

/**
 * The row a bulk download persists: an app-authored task whose `inputs` carries
 * the selection config. Single marker in `task_source` (ADR-0011) — `service`
 * is left null; `source`/`source_id` scope it to the Portfolio (and attach it
 * to the portfolio's logs).
 */
export function buildBulkDownloadRecord(args: {
  portfolioId: string;
  config: BulkDownloadConfig;
}): {
  taskSource: typeof BULK_DOWNLOAD_TASK_SOURCE;
  source: "portfolio_id";
  sourceId: string;
  status: "waiting";
  inputs: string;
} {
  return {
    taskSource: BULK_DOWNLOAD_TASK_SOURCE,
    source: "portfolio_id",
    sourceId: args.portfolioId,
    status: "waiting",
    inputs: JSON.stringify(args.config),
  };
}

// ── Delivery / status (optional in-app polling) ──────────────────────────────

export interface BulkDownloadSkipped {
  landlord_property_id: string;
  s3_key: string;
  reason: string;
}

export interface BulkDownloadDelivery {
  /** ZIP link, valid 60 minutes. */
  presignedUrl: string;
  packageS3Key: string | null;
  /** Documents included. */
  included: number;
  skipped: BulkDownloadSkipped[];
}

export type BulkDownloadState = "preparing" | "ready" | "failed";

export interface BulkDownloadStatus {
  state: BulkDownloadState;
  delivery: BulkDownloadDelivery | null;
}

/**
 * Read the worker's `sub_task.outputs` back into the app's shape. Null for
 * anything that isn't a finished delivery (in-flight, malformed) — polling must
 * never throw on an unfinished row.
 */
export function parseDelivery(outputs: string | null): BulkDownloadDelivery | null {
  if (!outputs) return null;
  try {
    const raw = JSON.parse(outputs) as Record<string, unknown>;
    // Tolerate link-key naming variants so a completed job surfaces its link.
    const url = [raw.presigned_url, raw.presignedUrl, raw.download_url, raw.url].find(
      (v): v is string => typeof v === "string" && v.length > 0,
    );
    if (!url) return null;
    return {
      presignedUrl: url,
      packageS3Key:
        typeof raw.package_s3_key === "string"
          ? raw.package_s3_key
          : typeof raw.s3_key === "string"
            ? raw.s3_key
            : null,
      included: typeof raw.included === "number" ? raw.included : 0,
      skipped: Array.isArray(raw.skipped)
        ? (raw.skipped as BulkDownloadSkipped[])
        : [],
    };
  } catch {
    return null;
  }
}

/**
 * Poll cadence: keep checking while the ZIP is being built, stop once it is
 * ready or failed. Shape matches TanStack v4's refetchInterval callback, which
 * receives the query data first.
 */
export function bulkDownloadRefetchInterval(
  data: BulkDownloadStatus | undefined,
): number | false {
  // Package assembly takes minutes, so a gentle cadence — email is the real
  // delivery channel; this poll is just an in-session convenience.
  if (!data) return 8_000;
  return data.state === "preparing" ? 8_000 : false;
}
