/**
 * PIBI-selection service (issue #254)
 *
 * Lets an approver mark which measures on a deal are going for PIBI. The
 * selection is recorded locally as `user_defined_deal_measures` rows with
 * `source = "pibi_ordered"` and pushed back to HubSpot under the
 * `measures_for_pibi_ordered` deal property via `syncMeasuresFieldToHubSpot`.
 *
 * Semantics deliberately mirror instruct-measure (issue #253):
 *  - The incoming `measureNames[]` is the NEW desired set, not a delta. The
 *    service REPLACES all existing `pibi_ordered` rows for the deal, then
 *    re-inserts one row per selected measure.
 *  - No `deal_measure_approvals` rows are created or modified.
 *  - `pushed_at` is stamped on every new row when HubSpot sync succeeds;
 *    left null on failure so a reconcile job can retry.
 *  - HubSpot push failures do NOT roll back the DB.
 *
 * The service accepts injectable deps so it stays environment-free in tests.
 */
import { and, eq } from "drizzle-orm";
import { db } from "@/app/db/db";
import { userDefinedDealMeasures } from "@/app/db/schema/user_defined_deal_measures";
import { syncMeasuresFieldToHubSpot as defaultSyncMeasuresField } from "@/app/lib/hubspot/dealSync";

export const PIBI_MEASURES_PROP = "measures_for_pibi_ordered";

// ---------------------------------------------------------------------------
// Injected dependency types — mirrors the pattern in instructMeasure.ts so
// the service is fully testable without touching the real DB or HubSpot.
// ---------------------------------------------------------------------------

export type SyncMeasuresField = typeof defaultSyncMeasuresField;

/**
 * Replace the pibi_ordered rows for the deal inside a transaction.
 * Returns the IDs of the newly inserted rows (one per selected measure).
 */
export type RunPibiTx = (params: {
  dealId: string;
  measureNames: string[];
  userId: bigint;
}) => Promise<{ insertedRowIds: bigint[] }>;

export type StampPushedAt = (rowIds: bigint[]) => Promise<void>;

// ---------------------------------------------------------------------------
// Public result type
// ---------------------------------------------------------------------------

export type SelectPibiMeasuresResult =
  | {
      ok: true;
      insertedRowIds: bigint[];
      hubspotSync: "ok" | "failed";
      hubspotError?: string;
    }
  | { ok: false; error: string };

// ---------------------------------------------------------------------------
// Input
// ---------------------------------------------------------------------------

export interface SelectPibiMeasuresInput {
  dealId: string;
  /** The FULL desired set of PIBI measures (replaces any prior selection). */
  measureNames: string[];
  userId: bigint;
  deps?: {
    runPibiTx?: RunPibiTx;
    syncMeasuresField?: SyncMeasuresField;
    stampPushedAt?: StampPushedAt;
  };
}

// ---------------------------------------------------------------------------
// Default DB-backed implementations
// ---------------------------------------------------------------------------

const defaultRunPibiTx: RunPibiTx = async ({ dealId, measureNames, userId }) => {
  return await db.transaction(async (tx) => {
    // Delete ALL existing pibi_ordered rows for this deal so the new
    // selection fully replaces the previous one.
    await tx
      .delete(userDefinedDealMeasures)
      .where(
        and(
          eq(userDefinedDealMeasures.hubspotDealId, dealId),
          eq(userDefinedDealMeasures.source, "pibi_ordered"),
        ),
      );

    if (measureNames.length === 0) {
      return { insertedRowIds: [] };
    }

    const inserted = await tx
      .insert(userDefinedDealMeasures)
      .values(
        measureNames.map((measureName) => ({
          hubspotDealId: dealId,
          measureName,
          source: "pibi_ordered" as const,
          createdByUserId: userId,
        })),
      )
      .returning({ id: userDefinedDealMeasures.id });

    return { insertedRowIds: inserted.map((r) => r.id) };
  });
};

const defaultStampPushedAt: StampPushedAt = async (rowIds) => {
  if (rowIds.length === 0) return;
  for (const rowId of rowIds) {
    await db
      .update(userDefinedDealMeasures)
      .set({ pushedAt: new Date() })
      .where(eq(userDefinedDealMeasures.id, rowId));
  }
};

// ---------------------------------------------------------------------------
// Service entry-point
// ---------------------------------------------------------------------------

export async function selectPibiMeasures(
  input: SelectPibiMeasuresInput,
): Promise<SelectPibiMeasuresResult> {
  const runPibiTx = input.deps?.runPibiTx ?? defaultRunPibiTx;
  const syncMeasuresField =
    input.deps?.syncMeasuresField ?? defaultSyncMeasuresField;
  const stampPushedAt = input.deps?.stampPushedAt ?? defaultStampPushedAt;

  // -------------------------------------------------------------------------
  // DB transaction. Any throw here rolls everything back — no row is
  // touched, no HubSpot push happens.
  // -------------------------------------------------------------------------
  let txResult: { insertedRowIds: bigint[] };
  try {
    txResult = await runPibiTx({
      dealId: input.dealId,
      measureNames: input.measureNames,
      userId: input.userId,
    });
  } catch (err) {
    const message =
      err instanceof Error ? err.message : "Failed to save PIBI measures";
    console.error("[selectPibiMeasures] transaction failed", {
      dealId: input.dealId,
      error: err,
    });
    return { ok: false, error: message };
  }

  // -------------------------------------------------------------------------
  // Post-commit: push the new selection list to HubSpot. Failures here do
  // NOT roll back the DB; `pushed_at` simply stays null.
  // -------------------------------------------------------------------------
  const syncResult = await syncMeasuresField({
    hubspotDealId: input.dealId,
    propName: PIBI_MEASURES_PROP,
    measureNames: input.measureNames,
  });

  if (syncResult.ok) {
    try {
      await stampPushedAt(txResult.insertedRowIds);
    } catch (err) {
      console.error("[selectPibiMeasures] failed to stamp pushed_at", {
        rowIds: txResult.insertedRowIds.map(String),
        error: err,
      });
    }
    return {
      ok: true,
      insertedRowIds: txResult.insertedRowIds,
      hubspotSync: "ok",
    };
  }

  return {
    ok: true,
    insertedRowIds: txResult.insertedRowIds,
    hubspotSync: "failed",
    hubspotError: syncResult.error,
  };
}
