import { createRoute } from "../../../../../modules/api-utils/createRoute";
import { opportunities } from "../../../../../managers/opportunities";
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../../../middleware/authorization";
import { processObjectValuePerms } from "../../../../../modules/permission-utils/processObjectPermissions";
import { ForecastProductController } from "../../../../../controllers/ForecastProductController";
import { opportunityCw } from "../../../../../modules/cw-utils/opportunities/opportunities";
import { z } from "zod";

const productItemSchema = z
  .object({
    catalogItem: z.object({ id: z.number().int().positive() }).optional(),
    forecastDescription: z.string().optional(),
    productDescription: z.string().optional(),
    customerDescription: z.string().nullable().optional(),
    quantity: z.number().positive().optional(),
    status: z.object({ id: z.number().int().positive() }).optional(),
    productClass: z.string().optional(),
    forecastType: z.string().optional(),
    revenue: z.number().optional(),
    cost: z.number().optional(),
    includeFlag: z.boolean().optional(),
    linkFlag: z.boolean().optional(),
    recurringFlag: z.boolean().optional(),
    taxableFlag: z.boolean().optional(),
    recurringRevenue: z.number().optional(),
    recurringCost: z.number().optional(),
    cycles: z.number().int().min(0).optional(),
    sequenceNumber: z.number().int().min(0).optional(),
    procurementNotes: z.string().optional(),
    productNarrative: z.string().optional(),
  })
  .strict();

const addProductSchema = z.union([
  productItemSchema,
  z.array(productItemSchema).min(1, "At least one product is required"),
]);

const procurementCreateSchema = z.object({
  catalogItem: z.object({ id: z.number().int().positive() }),
  description: z.string().min(1),
  customerDescription: z.string().optional(),
  quantity: z.number().positive().optional(),
  price: z.number().optional(),
  cost: z.number().optional(),
  taxableFlag: z.boolean().optional(),
  billableOption: z.string().optional(),
  procurementNotes: z.string().optional(),
  productNarrative: z.string().optional(),
});

/* POST /v1/sales/opportunities/opportunity/:identifier/products */
export default createRoute(
  "post",
  ["/opportunities/opportunity/:identifier/products"],
  async (c) => {
    const identifier = c.req.param("identifier") as string;
    const body = await c.req.json();

    const validated = addProductSchema.parse(body);
    const inputItems = Array.isArray(validated) ? validated : [validated];

    // Gate each submitted field against user permissions.
    // Only fields the user has permission for are forwarded to ConnectWise.
    const user = c.get("user");
    const gatedItems = await Promise.all(
      inputItems.map((item) =>
        processObjectValuePerms(item, "sales.opportunity.product.field", user),
      ),
    );

    const item = await opportunities.fetchRecord(identifier);

    // Procurement-first: when catalogItem is available after field-level
    // permission gating, create through procurement.
    // Fallback: if catalogItem is gated/missing, use forecast creation so
    // callers without catalog permissions can still add products.
    const procurementInputs: Array<{
      index: number;
      payload: z.infer<typeof procurementCreateSchema>;
    }> = [];
    const forecastInputs: Array<{
      index: number;
      payload: Record<string, unknown>;
      customerDescription?: string;
    }> = [];

    for (const [index, g] of gatedItems.entries()) {
      const hasCatalogItem =
        typeof g?.catalogItem?.id === "number" && g.catalogItem.id > 0;

      if (!hasCatalogItem) {
        const { customerDescription, ...forecastPayload } = g as any;
        const normalizedForecastPayload: Record<string, unknown> = {
          ...forecastPayload,
        };

        const hasAnyDescription =
          typeof normalizedForecastPayload.forecastDescription === "string" ||
          typeof normalizedForecastPayload.productDescription === "string";

        // CW rejects bare forecast objects with only defaults (status/type).
        // Ensure a minimal description exists for permission-reduced payloads.
        if (!hasAnyDescription) {
          normalizedForecastPayload.forecastDescription = "Line Item";
        }

        forecastInputs.push({
          index,
          payload: normalizedForecastPayload,
          customerDescription:
            typeof customerDescription === "string"
              ? customerDescription
              : undefined,
        });
        continue;
      }

      const quantity =
        typeof g.quantity === "number" && g.quantity > 0 ? g.quantity : undefined;
      const totalRevenue =
        typeof g.revenue === "number" && Number.isFinite(g.revenue)
          ? g.revenue
          : undefined;
      const totalCost =
        typeof g.cost === "number" && Number.isFinite(g.cost)
          ? g.cost
          : undefined;

      const price =
        totalRevenue === undefined
          ? undefined
          : quantity && quantity > 0
            ? totalRevenue / quantity
            : totalRevenue;
      const cost =
        totalCost === undefined
          ? undefined
          : quantity && quantity > 0
            ? totalCost / quantity
            : totalCost;

      procurementInputs.push({
        index,
        payload: procurementCreateSchema.parse({
          catalogItem: g.catalogItem,
          description:
            g.productDescription ??
            g.forecastDescription ??
            g.customerDescription ??
            "Line Item",
          customerDescription:
            typeof g.customerDescription === "string"
              ? g.customerDescription
              : undefined,
          quantity,
          price,
          cost,
          taxableFlag:
            typeof g.taxableFlag === "boolean" ? g.taxableFlag : undefined,
          billableOption: "Billable",
          procurementNotes:
            typeof (g as any).procurementNotes === "string"
              ? (g as any).procurementNotes
              : undefined,
          productNarrative:
            typeof (g as any).productNarrative === "string"
              ? (g as any).productNarrative
              : undefined,
        }),
      });
    }

    const createdByIndex = new Map<number, ForecastProductController>();

    if (procurementInputs.length > 0) {
      const createdProcurement = await item.addProcurementProducts(
        procurementInputs.map((entry) => entry.payload),
      );

      const indexByForecastId = new Map<number, number>();
      for (const [createdIdx, proc] of createdProcurement.entries()) {
        if (typeof proc.forecastDetailId === "number") {
          indexByForecastId.set(
            proc.forecastDetailId,
            procurementInputs[createdIdx]!.index,
          );
        }
      }

      if (indexByForecastId.size > 0) {
        const createdForecastItems =
          (await opportunityCw.fetchProducts(item.cwOpportunityId)).forecastItems?.filter(
            (fi) => indexByForecastId.has(fi.id),
          ) ?? [];

        for (const fi of createdForecastItems) {
          const originalIndex = indexByForecastId.get(fi.id);
          if (typeof originalIndex === "number") {
            createdByIndex.set(originalIndex, new ForecastProductController(fi));
          }
        }
      }
    }

    if (forecastInputs.length > 0) {
      const createdForecast = await item.addProducts(
        forecastInputs.map((entry) => entry.payload),
      );

      for (const [createdIdx, createdItem] of createdForecast.entries()) {
        const input = forecastInputs[createdIdx]!;
        createdByIndex.set(input.index, createdItem);
      }

      const procurementUpdates = createdForecast
        .map((product, idx) => ({
          product,
          customerDescription: forecastInputs[idx]?.customerDescription,
        }))
        .filter((entry) => entry.customerDescription != null);

      if (procurementUpdates.length > 0) {
        await Promise.all(
          procurementUpdates.map(({ product, customerDescription }) =>
            item
              .updateProcurementProductByForecastItem(product.cwForecastId, {
                customerDescription,
              })
              .catch(() => null),
          ),
        );
      }
    }

    const created = inputItems
      .map((_, index) => createdByIndex.get(index))
      .filter(
        (entry): entry is ForecastProductController => entry !== undefined,
      );

    const isBatch = Array.isArray(body);
    const response = apiResponse.created(
      isBatch
        ? `${created.length} product(s) added to opportunity successfully!`
        : "Product added to opportunity successfully!",
      isBatch
        ? created.map((p) => p.toJson())
        : created[0]?.toJson() ?? null,
    );
    return c.json(response, response.status as ContentfulStatusCode);
  },
  authMiddleware({ permissions: ["sales.opportunity.product.add"] }),
);
