/**
 * Test Script: CW Forecast Item Edit & Partial Cancellation
 *
 * This script performs read-write operations against the ConnectWise API:
 *
 *   1. Search all open opportunities for a forecast item with description
 *      matching "labor Special Order" (case-insensitive).
 *   2. Report the current state of that item (price, cost, qty, etc.).
 *   3. PATCH the item: revenue → 72,000 | cost → 8,500 | quantity → 67
 *   4. Verify the update by re-fetching the forecast.
 *   5. Cancel 13 units via the linked procurement product
 *      (partial cancellation: quantityCancelled = 13).
 *   6. Verify the cancellation by re-fetching procurement data.
 *   7. Report on every step.
 *
 * Usage:  bun run test-cw-edit-item.ts
 */

import axios from "axios";

const cw = axios.create({
  baseURL: "https://ttscw.totaltech.net/v4_6_release/apis/3.0/",
  headers: {
    Authorization: `Basic ${process.env.CW_BASIC_TOKEN}`,
    clientId: `${process.env.CW_CLIENT_ID}`,
    "Content-Type": "application/json",
  },
  timeout: 30_000,
});

// ── Helpers ───────────────────────────────────────────────────────────────────

const log = (label: string, ...args: unknown[]) =>
  console.log(`\n[${label}]`, ...args);

const divider = () => console.log("─".repeat(72));

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

const fmt = (n: number) =>
  n.toLocaleString("en-US", {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  });

// ── Types (minimal, for this script) ──────────────────────────────────────────

interface ForecastItem {
  id: number;
  forecastDescription: string;
  productDescription: string;
  quantity: number;
  revenue: number;
  cost: number;
  margin: number;
  forecastType: string;
  sequenceNumber: number;
  catalogItem?: { id: number; identifier: string };
  status?: { id: number; name: string };
  opportunity?: { id: number; name: string };
  [key: string]: unknown;
}

interface Forecast {
  id: number;
  forecastItems: ForecastItem[];
  [key: string]: unknown;
}

interface ProcurementProduct {
  id: number;
  forecastDetailId: number;
  description: string;
  quantity: number;
  price: number;
  cost: number;
  cancelledFlag: boolean;
  quantityCancelled: number;
  cancelledReason: string | null;
  cancelledBy: string | null;
  cancelledDate: string | null;
  opportunity?: { id: number };
  [key: string]: unknown;
}

// ── Main ──────────────────────────────────────────────────────────────────────

async function main() {
  divider();
  log("START", "CW Forecast Item Edit & Cancellation Test");
  log("START", `Timestamp: ${new Date().toISOString()}`);
  divider();

  // ── Step 1: Find the "labor Special Order" forecast item ────────────────

  const OPP_ID = 5150;
  log(
    "SEARCH",
    `Looking for forecast item matching "labor Special Order" on opportunity ${OPP_ID}...`,
  );

  // Fetch the forecast for opportunity 5150 directly
  let targetOppId: number = OPP_ID;
  let targetItem: ForecastItem | null = null;
  let targetForecast: Forecast | null = null;

  const forecastRes = await cw.get(`/sales/opportunities/${OPP_ID}/forecast`);
  targetForecast = forecastRes.data as Forecast;
  const match = (targetForecast.forecastItems ?? []).find(
    (fi: ForecastItem) =>
      fi.forecastDescription?.toLowerCase().includes("special order") ||
      fi.productDescription?.toLowerCase().includes("special order"),
  );

  if (match) {
    targetItem = match;
    log("SEARCH", `✓ FOUND forecast item on opportunity ${OPP_ID}`);
  }

  if (!targetItem || !targetForecast) {
    log(
      "SEARCH",
      `✗ No "labor Special Order" item found on opportunity ${OPP_ID}.`,
    );
    log("SEARCH", "All forecast items on this opportunity:");
    for (const fi of targetForecast.forecastItems ?? []) {
      console.log(
        `  id=${fi.id} "${fi.forecastDescription}" / "${fi.productDescription}"`,
      );
    }
    log("SEARCH", "Aborting.");
    process.exit(1);
  }

  // ── Step 2: Report current state ────────────────────────────────────────

  divider();
  log("CURRENT STATE", "Forecast item details BEFORE edit:");
  console.log(`  Opportunity ID:       ${targetOppId}`);
  console.log(`  Forecast Item ID:     ${targetItem.id}`);
  console.log(`  Forecast Description: ${targetItem.forecastDescription}`);
  console.log(`  Product Description:  ${targetItem.productDescription}`);
  console.log(
    `  Catalog Item:         ${targetItem.catalogItem?.identifier ?? "(none)"} (cwId=${targetItem.catalogItem?.id ?? "N/A"})`,
  );
  console.log(`  Forecast Type:        ${targetItem.forecastType}`);
  console.log(
    `  Status:               ${targetItem.status?.name ?? "?"} (id=${targetItem.status?.id ?? "?"})`,
  );
  console.log(`  Sequence Number:      ${targetItem.sequenceNumber}`);
  console.log(`  ──────────────────────────────────`);
  console.log(`  Quantity:             ${targetItem.quantity}`);
  console.log(`  Revenue (Price):      $${fmt(targetItem.revenue)}`);
  console.log(`  Cost:                 $${fmt(targetItem.cost)}`);
  console.log(`  Margin:               $${fmt(targetItem.margin)}`);

  // Also report all items on this opportunity for context
  const allItems = targetForecast.forecastItems ?? [];
  log(
    "CONTEXT",
    `Total forecast items on this opportunity: ${allItems.length}`,
  );
  for (const fi of allItems) {
    const marker = fi.id === targetItem.id ? " ◀ TARGET" : "";
    console.log(
      `  [${fi.sequenceNumber}] id=${fi.id} "${fi.forecastDescription}" ` +
        `qty=${fi.quantity} rev=$${fmt(fi.revenue)} cost=$${fmt(fi.cost)}${marker}`,
    );
  }

  // ── Step 3: PATCH the forecast item ─────────────────────────────────────

  divider();
  const UNIT_PRICE = 72_000;
  const UNIT_COST = 8_500;
  const QTY = 67;
  const TOTAL_REVENUE = UNIT_PRICE * QTY; // $4,824,000
  const TOTAL_COST = UNIT_COST * QTY; // $569,500

  log("EDIT", "Patching forecast item...");
  log(
    "EDIT",
    `  Unit price: $${fmt(UNIT_PRICE)} × ${QTY} = $${fmt(TOTAL_REVENUE)} (revenue)`,
  );
  log(
    "EDIT",
    `  Unit cost:  $${fmt(UNIT_COST)} × ${QTY} = $${fmt(TOTAL_COST)} (cost)`,
  );
  log("EDIT", `  Quantity:   ${QTY}`);

  // Find the index of our target item in the forecast array
  const forecastItems = targetForecast.forecastItems ?? [];
  const targetIdx = forecastItems.findIndex((fi) => fi.id === targetItem!.id);

  if (targetIdx === -1) {
    log(
      "EDIT",
      "✗ Could not find target item index in forecast array. Aborting.",
    );
    process.exit(1);
  }

  log("EDIT", `Target item is at index ${targetIdx} in forecastItems array.`);

  const patchOps = [
    {
      op: "replace",
      path: `/forecastItems/${targetIdx}/revenue`,
      value: TOTAL_REVENUE,
    },
    {
      op: "replace",
      path: `/forecastItems/${targetIdx}/cost`,
      value: TOTAL_COST,
    },
    { op: "replace", path: `/forecastItems/${targetIdx}/quantity`, value: QTY },
  ];

  log("EDIT", "Patch operations:");
  for (const op of patchOps) {
    console.log(`  ${op.op} ${op.path} → ${op.value}`);
  }

  try {
    const patchRes = await cw.patch(
      `/sales/opportunities/${targetOppId}/forecast`,
      patchOps,
    );
    const updatedForecast: Forecast = patchRes.data;
    const updatedItem = (updatedForecast.forecastItems ?? [])[targetIdx];

    if (!updatedItem) {
      log("EDIT", "✗ Item not found at expected index after PATCH.");
    } else {
      log("EDIT", "✓ PATCH successful. Updated item:");
      console.log(`  Forecast Item ID:     ${updatedItem.id}`);
      console.log(`  Forecast Description: ${updatedItem.forecastDescription}`);
      console.log(`  Quantity:             ${updatedItem.quantity}`);
      console.log(`  Revenue (Price):      $${fmt(updatedItem.revenue)}`);
      console.log(`  Cost:                 $${fmt(updatedItem.cost)}`);
      console.log(`  Margin:               $${fmt(updatedItem.margin)}`);

      // Verify values match what we set
      const checks = [
        {
          field: "revenue",
          expected: TOTAL_REVENUE,
          actual: updatedItem.revenue,
        },
        { field: "cost", expected: TOTAL_COST, actual: updatedItem.cost },
        { field: "quantity", expected: QTY, actual: updatedItem.quantity },
      ];

      log("VERIFY EDIT", "Checking values match requested:");
      for (const check of checks) {
        const ok = check.actual === check.expected;
        console.log(
          `  ${ok ? "✓" : "✗"} ${check.field}: expected=${check.expected}, actual=${check.actual}`,
        );
      }

      // Update our reference for the cancellation step
      targetItem = updatedItem;
    }
  } catch (err: any) {
    log("EDIT", `✗ PATCH failed: ${err.response?.status ?? err.message}`);
    if (err.response?.data) {
      console.log("  Response:", JSON.stringify(err.response.data, null, 2));
    }

    // If quantity PATCH failed (read-only), try without quantity
    if (err.response?.status === 400 || err.response?.status === 422) {
      log(
        "EDIT",
        "Retrying without quantity (may be read-only on forecast items)...",
      );
      const retryOps = patchOps.filter((op) => !op.path.endsWith("/quantity"));
      try {
        const retryRes = await cw.patch(
          `/sales/opportunities/${targetOppId}/forecast`,
          retryOps,
        );
        const retryForecast: Forecast = retryRes.data;
        const retryItem = (retryForecast.forecastItems ?? [])[targetIdx];

        if (retryItem) {
          log(
            "EDIT",
            "✓ Retry PATCH successful (without quantity). Updated item:",
          );
          console.log(
            `  Quantity:        ${retryItem.quantity} (unchanged — read-only)`,
          );
          console.log(`  Revenue (Price): $${fmt(retryItem.revenue)}`);
          console.log(`  Cost:            $${fmt(retryItem.cost)}`);
          console.log(`  Margin:          $${fmt(retryItem.margin)}`);
          targetItem = retryItem;
        }
      } catch (retryErr: any) {
        log(
          "EDIT",
          `✗ Retry also failed: ${retryErr.response?.status ?? retryErr.message}`,
        );
        if (retryErr.response?.data) {
          console.log(
            "  Response:",
            JSON.stringify(retryErr.response.data, null, 2),
          );
        }
      }
    }
  }

  // ── Step 4: Re-fetch and confirm final forecast state ───────────────────

  divider();
  log("RE-FETCH", "Fetching forecast to confirm final state...");
  await sleep(500);

  const confirmRes = await cw.get(
    `/sales/opportunities/${targetOppId}/forecast`,
  );
  const confirmedForecast: Forecast = confirmRes.data;
  const confirmedItem = (confirmedForecast.forecastItems ?? []).find(
    (fi) => fi.id === targetItem!.id,
  );

  if (confirmedItem) {
    log("CONFIRMED STATE", "Forecast item after edit:");
    console.log(`  Forecast Item ID:     ${confirmedItem.id}`);
    console.log(`  Forecast Description: ${confirmedItem.forecastDescription}`);
    console.log(`  Quantity:             ${confirmedItem.quantity}`);
    console.log(`  Revenue (Price):      $${fmt(confirmedItem.revenue)}`);
    console.log(`  Cost:                 $${fmt(confirmedItem.cost)}`);
    console.log(`  Margin:               $${fmt(confirmedItem.margin)}`);
  } else {
    log(
      "CONFIRMED STATE",
      "⚠ Could not find item by original ID — it may have been regenerated.",
    );
    log("CONFIRMED STATE", "All current forecast items:");
    for (const fi of confirmedForecast.forecastItems ?? []) {
      console.log(
        `  id=${fi.id} "${fi.forecastDescription}" qty=${fi.quantity} rev=$${fmt(fi.revenue)} cost=$${fmt(fi.cost)}`,
      );
    }
  }

  // ── Step 5: Cancel 13 items via procurement product ─────────────────────

  divider();
  log("CANCEL", "Cancelling 13 units on this item via procurement product...");

  // First, find existing procurement products linked to this opportunity
  const procRes = await cw.get(
    `/procurement/products?conditions=${encodeURIComponent(`opportunity/id=${targetOppId}`)}&pageSize=1000`,
  );
  const procProducts: ProcurementProduct[] = procRes.data;

  log(
    "CANCEL",
    `Found ${procProducts.length} procurement product(s) on this opportunity.`,
  );

  if (procProducts.length > 0) {
    for (const pp of procProducts) {
      console.log(
        `  Proc id=${pp.id} forecastDetailId=${pp.forecastDetailId} ` +
          `"${pp.description}" qty=${pp.quantity} price=$${fmt(pp.price ?? 0)} ` +
          `cancelled=${pp.cancelledFlag} qtyCancelled=${pp.quantityCancelled}`,
      );
    }
  }

  // Find the procurement product linked to our forecast item
  const linkedProc = procProducts.find(
    (pp) => pp.forecastDetailId === targetItem!.id,
  );

  if (linkedProc) {
    log("CANCEL", `Found linked procurement product: id=${linkedProc.id}`);
    log(
      "CANCEL",
      `Current state: cancelled=${linkedProc.cancelledFlag}, quantityCancelled=${linkedProc.quantityCancelled}`,
    );
    log("CANCEL", "Patching: quantityCancelled → 13, cancelledFlag → true");

    try {
      const cancelRes = await cw.patch(
        `/procurement/products/${linkedProc.id}`,
        [
          { op: "replace", path: "cancelledFlag", value: true },
          { op: "replace", path: "quantityCancelled", value: 13 },
          {
            op: "replace",
            path: "cancelledReason",
            value: "Test cancellation — 13 units",
          },
        ],
      );

      log("CANCEL", "✓ Cancellation PATCH successful.");
      console.log(`  cancelledFlag:      ${cancelRes.data.cancelledFlag}`);
      console.log(`  quantityCancelled:  ${cancelRes.data.quantityCancelled}`);
      console.log(`  cancelledReason:    ${cancelRes.data.cancelledReason}`);
      console.log(
        `  cancelledBy:        ${cancelRes.data.cancelledBy ?? "N/A"}`,
      );
      console.log(
        `  cancelledDate:      ${cancelRes.data.cancelledDate ?? "N/A"}`,
      );
    } catch (err: any) {
      log(
        "CANCEL",
        `✗ Cancellation PATCH failed: ${err.response?.status ?? err.message}`,
      );
      if (err.response?.data) {
        console.log("  Response:", JSON.stringify(err.response.data, null, 2));
      }
    }
  } else {
    log(
      "CANCEL",
      `No procurement product linked to forecast item id=${targetItem!.id}.`,
    );
    log(
      "CANCEL",
      "Creating a procurement product first, then cancelling 13...",
    );

    try {
      // Create a procurement product linked to this forecast item
      const createProcRes = await cw.post("/procurement/products", {
        catalogItem: targetItem!.catalogItem?.id
          ? { id: targetItem!.catalogItem.id }
          : undefined,
        description:
          targetItem!.forecastDescription || targetItem!.productDescription,
        quantity: targetItem!.quantity || 67,
        price: targetItem!.revenue || 72_000,
        cost: targetItem!.cost || 8_500,
        billableOption: "Billable",
        opportunity: { id: targetOppId },
        forecastDetailId: targetItem!.id,
      });

      const newProc = createProcRes.data;
      log("CANCEL", `✓ Created procurement product id=${newProc.id}`);
      console.log(`  forecastDetailId: ${newProc.forecastDetailId}`);
      console.log(`  description:      ${newProc.description}`);
      console.log(`  quantity:         ${newProc.quantity}`);
      console.log(`  price:            $${fmt(newProc.price ?? 0)}`);
      console.log(`  cost:             $${fmt(newProc.cost ?? 0)}`);

      // Now cancel 13 units
      log("CANCEL", "Patching procurement product: quantityCancelled → 13...");
      const cancelRes = await cw.patch(`/procurement/products/${newProc.id}`, [
        { op: "replace", path: "cancelledFlag", value: true },
        { op: "replace", path: "quantityCancelled", value: 13 },
        {
          op: "replace",
          path: "cancelledReason",
          value: "Test cancellation — 13 units",
        },
      ]);

      log("CANCEL", "✓ Cancellation PATCH successful.");
      console.log(`  cancelledFlag:      ${cancelRes.data.cancelledFlag}`);
      console.log(`  quantityCancelled:  ${cancelRes.data.quantityCancelled}`);
      console.log(`  cancelledReason:    ${cancelRes.data.cancelledReason}`);
      console.log(
        `  cancelledBy:        ${cancelRes.data.cancelledBy ?? "N/A"}`,
      );
      console.log(
        `  cancelledDate:      ${cancelRes.data.cancelledDate ?? "N/A"}`,
      );
    } catch (err: any) {
      log("CANCEL", `✗ Failed: ${err.response?.status ?? err.message}`);
      if (err.response?.data) {
        console.log("  Response:", JSON.stringify(err.response.data, null, 2));
      }
    }
  }

  // ── Step 6: Final verification ──────────────────────────────────────────

  divider();
  log("FINAL VERIFY", "Re-fetching all data for final report...");
  await sleep(500);

  // Re-fetch forecast
  const finalForecastRes = await cw.get(
    `/sales/opportunities/${targetOppId}/forecast`,
  );
  const finalForecast: Forecast = finalForecastRes.data;
  const finalItem =
    (finalForecast.forecastItems ?? []).find(
      (fi) => fi.id === targetItem!.id,
    ) ??
    (finalForecast.forecastItems ?? []).find(
      (fi) =>
        fi.forecastDescription?.toLowerCase().includes("special order") ||
        fi.productDescription?.toLowerCase().includes("special order"),
    );

  // Re-fetch procurement
  const finalProcRes = await cw.get(
    `/procurement/products?conditions=${encodeURIComponent(`opportunity/id=${targetOppId}`)}&pageSize=1000`,
  );
  const finalProcs: ProcurementProduct[] = finalProcRes.data;

  log("FINAL STATE — FORECAST ITEM", "");
  if (finalItem) {
    console.log(`  Forecast Item ID:     ${finalItem.id}`);
    console.log(`  Forecast Description: ${finalItem.forecastDescription}`);
    console.log(`  Quantity:             ${finalItem.quantity}`);
    console.log(`  Revenue (Price):      $${fmt(finalItem.revenue)}`);
    console.log(`  Cost:                 $${fmt(finalItem.cost)}`);
    console.log(`  Margin:               $${fmt(finalItem.margin)}`);
  } else {
    console.log("  ⚠ Target item not found in final forecast.");
  }

  log("FINAL STATE — PROCUREMENT", `${finalProcs.length} product(s):`);
  for (const pp of finalProcs) {
    console.log(
      `  id=${pp.id} forecastDetailId=${pp.forecastDetailId} ` +
        `"${pp.description}" qty=${pp.quantity} cancelled=${pp.cancelledFlag} ` +
        `qtyCancelled=${pp.quantityCancelled} reason="${pp.cancelledReason ?? ""}"`,
    );
  }

  // ── Summary ─────────────────────────────────────────────────────────────

  divider();
  log("SUMMARY", "");

  // After cancelling 13 of 67, CW recalculates totals for remaining 54 units
  const expectedFinalRevenue = Math.round(UNIT_PRICE * (QTY - 13) * 100) / 100;
  const expectedFinalCost = Math.round(UNIT_COST * (QTY - 13) * 100) / 100;

  const editOk = finalItem
    ? Math.abs(finalItem.revenue - expectedFinalRevenue) < 1 &&
      Math.abs(finalItem.cost - expectedFinalCost) < 1
    : false;
  const qtyOk = finalItem ? finalItem.quantity === QTY : false;

  if (finalItem) {
    console.log(
      `  Expected final revenue ($${fmt(UNIT_PRICE)} × ${QTY - 13}): $${fmt(expectedFinalRevenue)}`,
    );
    console.log(
      `  Actual final revenue:                          $${fmt(finalItem.revenue)}`,
    );
    console.log(
      `  Expected final cost ($${fmt(UNIT_COST)} × ${QTY - 13}):    $${fmt(expectedFinalCost)}`,
    );
    console.log(
      `  Actual final cost:                             $${fmt(finalItem.cost)}`,
    );
  }
  const cancelOk = finalProcs.some(
    (pp) =>
      pp.forecastDetailId === targetItem!.id &&
      pp.cancelledFlag === true &&
      pp.quantityCancelled === 13,
  );

  console.log(
    `  Unit price $${fmt(UNIT_PRICE)}/ea:     `,
    editOk ? "✓ PASS" : "✗ FAIL",
  );
  console.log(
    `  Unit cost $${fmt(UNIT_COST)}/ea:        `,
    editOk ? "✓ PASS" : "✗ FAIL",
  );
  console.log(
    `  Quantity set to ${QTY}:           `,
    qtyOk ? "✓ PASS" : "✗ FAIL (may be read-only)",
  );
  console.log(
    "  13 units cancelled:           ",
    cancelOk ? "✓ PASS" : "✗ FAIL",
  );

  const allPass = editOk && qtyOk && cancelOk;
  divider();
  log(
    "RESULT",
    allPass
      ? "✓ ALL CHECKS PASSED"
      : "⚠ SOME CHECKS DID NOT PASS — review output above",
  );
  divider();
}

main().catch((err) => {
  console.error("\n[FATAL]", err.response?.data ?? err.message);
  process.exit(1);
});
