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 GenericError from "../../../../../Errors/GenericError";
import { ForecastProductController } from "../../../../../controllers/ForecastProductController";
import { opportunityCw } from "../../../../../modules/cw-utils/opportunities/opportunities";
import { prisma } from "../../../../../constants";
import { z } from "zod";

const PRODUCT_NARRATIVE_FIELD_ID = 46;
const PROCUREMENT_NOTES_FIELD_ID = 29;

const updateProductSchema = z
  .object({
    productDescription: z.string().min(1).optional(),
    quantity: z.number().positive().optional(),
    unitPrice: z.number().min(0).optional(),
    unitCost: z.number().min(0).optional(),
    customerDescription: z.string().nullable().optional(),
    productNarrative: z.string().nullable().optional(),
    procurementNotes: z.string().nullable().optional(),
    taxableFlag: z.boolean().optional(),
  })
  .strict()
  .refine(
    (value) =>
      Object.values(value).some((item) => item !== undefined && item !== null),
    "At least one editable field is required"
  );

const upsertCustomTextField = (
  fields: Array<Record<string, unknown>>,
  fieldId: number,
  caption: string,
  value: string
) => {
  const next = [...fields];
  const idx = next.findIndex((f) => Number(f.id) === fieldId);

  const field = {
    id: fieldId,
    caption,
    type: "Text",
    entryMethod: "EntryField",
    value,
  };

  if (idx === -1) {
    next.push(field);
    return next;
  }

  next[idx] = {
    ...next[idx],
    ...field,
  };
  return next;
};

/* PATCH /v1/sales/opportunities/opportunity/:identifier/products/:productId/edit */
export default createRoute(
  "patch",
  ["/opportunities/opportunity/:identifier/products/:productId/edit"],
  async (c) => {
    const identifier = c.req.param("identifier");
    const productId = Number(c.req.param("productId"));
    const body = await c.req.json();

    if (!Number.isInteger(productId) || productId <= 0) {
      throw new GenericError({
        status: 400,
        name: "InvalidProductId",
        message: "productId must be a positive integer",
      });
    }

    const input = updateProductSchema.parse(body);
    const opportunity = await opportunities.fetchRecord(identifier);

    const cwForecast = await opportunityCw.fetchProducts(
      opportunity.cwOpportunityId
    );
    const cwForecastItems = cwForecast.forecastItems ?? [];
    const cwForecastIds = new Set(cwForecastItems.map((item) => item.id));

    let forecastItemId = productId;
    if (!cwForecastIds.has(forecastItemId)) {
      const procurementItems = await opportunityCw.fetchProcurementProducts(
        opportunity.cwOpportunityId
      );

      const matchedProcurement = procurementItems.find(
        (item) => Number((item as any).id) === productId
      );
      const mappedForecastDetailId = Number(
        (matchedProcurement as any)?.forecastDetailId
      );

      if (
        Number.isInteger(mappedForecastDetailId) &&
        mappedForecastDetailId > 0 &&
        cwForecastIds.has(mappedForecastDetailId)
      ) {
        forecastItemId = mappedForecastDetailId;
        console.warn(
          "[ProductUpdate] Resolved procurement product ID to forecast item ID",
          {
            opportunity: identifier,
            requestedProductId: productId,
            resolvedForecastItemId: forecastItemId,
          }
        );
      }
    }

    const rawForecastItem = cwForecastItems.find(
      (item) => item.id === forecastItemId
    );

    if (!rawForecastItem) {
      throw new GenericError({
        status: 404,
        name: "ForecastItemNotFound",
        message: `Forecast item ${productId} not found on opportunity`,
      });
    }

    const forecastItem = new ForecastProductController(rawForecastItem);
    const forecastJson = forecastItem.toJson();
    const effectiveQuantity = input.quantity ?? forecastJson.quantity ?? 1;

    const forecastPatch: Record<string, unknown> = {};
    if (input.productDescription !== undefined) {
      forecastPatch.productDescription = input.productDescription;
    }
    if (input.quantity !== undefined) {
      forecastPatch.quantity = input.quantity;
    }
    if (input.unitPrice !== undefined) {
      forecastPatch.revenue = Number(
        (input.unitPrice * effectiveQuantity).toFixed(2)
      );
    }
    if (input.unitCost !== undefined) {
      forecastPatch.cost = Number(
        (input.unitCost * effectiveQuantity).toFixed(2)
      );
    }
    if (input.taxableFlag !== undefined) {
      forecastPatch.taxableFlag = input.taxableFlag;
    }

    const existingProcurement =
      await opportunity.fetchProcurementProductByForecastItem(forecastItemId);

    const hasNarrativeOrNotesValue =
      (input.productNarrative !== undefined &&
        input.productNarrative !== null) ||
      (input.procurementNotes !== undefined && input.procurementNotes !== null);

    if (hasNarrativeOrNotesValue && !existingProcurement) {
      console.warn(
        "[ProductUpdate] Ignoring procurement-only narrative fields for non-linked product",
        {
          opportunity: identifier,
          productId: forecastItemId,
          requestedProductId: productId,
        }
      );
    }

    let updatedProcurement = existingProcurement;
    if (existingProcurement) {
      const procurementPatch: Record<string, unknown> = {};
      if (input.productDescription !== undefined) {
        procurementPatch.description = input.productDescription;
      }
      if (input.quantity !== undefined) {
        procurementPatch.quantity = input.quantity;
      }
      if (input.unitPrice !== undefined) {
        procurementPatch.price = input.unitPrice;
      }
      if (input.unitCost !== undefined) {
        procurementPatch.cost = input.unitCost;
      }
      if (
        input.customerDescription !== undefined &&
        input.customerDescription !== null
      ) {
        procurementPatch.customerDescription = input.customerDescription;
      }

      const existingFields = Array.isArray(existingProcurement.customFields)
        ? existingProcurement.customFields.map((field) => ({ ...field }))
        : [];

      let updatedFields = existingFields as Array<Record<string, unknown>>;
      if (
        input.procurementNotes !== undefined &&
        input.procurementNotes !== null
      ) {
        updatedFields = upsertCustomTextField(
          updatedFields,
          PROCUREMENT_NOTES_FIELD_ID,
          "Procurement Notes",
          input.procurementNotes
        );
      }
      if (
        input.productNarrative !== undefined &&
        input.productNarrative !== null
      ) {
        updatedFields = upsertCustomTextField(
          updatedFields,
          PRODUCT_NARRATIVE_FIELD_ID,
          "Product Narrative",
          input.productNarrative
        );
      }
      if (
        (input.procurementNotes !== undefined &&
          input.procurementNotes !== null) ||
        (input.productNarrative !== undefined &&
          input.productNarrative !== null)
      ) {
        procurementPatch.customFields = updatedFields;
      }

      if (Object.keys(procurementPatch).length > 0) {
        updatedProcurement =
          await opportunity.updateProcurementProductByForecastItem(
            forecastItemId,
            procurementPatch
          );
      }
    }

    let updatedForecast = forecastJson;
    if (Object.keys(forecastPatch).length > 0) {
      const patched = await opportunity.updateProduct(
        forecastItemId,
        forecastPatch
      );
      updatedForecast = patched.toJson();
    }

    // Write changed fields back to the local ProductData row so fetchProducts()
    // reflects the edit immediately without waiting for the next dalpuri sync.
    const localPatch: Record<string, unknown> = {};
    if (input.quantity !== undefined) localPatch.qty = input.quantity;
    if (input.unitPrice !== undefined) localPatch.unitPrice = input.unitPrice;
    if (input.unitCost !== undefined) localPatch.unitCost = input.unitCost;
    if (input.productDescription !== undefined) localPatch.shortDescription = input.productDescription;
    if (input.taxableFlag !== undefined) localPatch.taxableFlag = input.taxableFlag;
    if (input.productNarrative !== undefined) localPatch.productNarrative = input.productNarrative;
    if (Object.keys(localPatch).length > 0) {
      await prisma.productData.update({
        where: { id: productId },
        data: localPatch,
      });
    }

    const updatedFields = Array.isArray(updatedProcurement?.customFields)
      ? updatedProcurement.customFields
      : [];
    const procurementNotes =
      updatedFields.find(
        (field: any) => field?.id === PROCUREMENT_NOTES_FIELD_ID
      )?.value ?? null;
    const productNarrative =
      updatedFields.find(
        (field: any) => field?.id === PRODUCT_NARRATIVE_FIELD_ID
      )?.value ?? null;

    const quantity =
      updatedProcurement?.quantity ?? updatedForecast.quantity ?? null;
    const unitPrice = updatedProcurement?.price ?? null;
    const unitCost = updatedProcurement?.cost ?? null;

    const response = apiResponse.successful("Product updated successfully!", {
      ...updatedForecast,
      productDescription:
        updatedProcurement?.description ?? updatedForecast.productDescription,
      customerDescription:
        updatedProcurement?.customerDescription ??
        updatedForecast.customerDescription ??
        null,
      quantity,
      unitPrice,
      unitCost,
      requestedProductId: productId,
      forecastItemId,
      procurementNotes,
      productNarrative,
    });

    return c.json(response, response.status as ContentfulStatusCode);
  },
  authMiddleware({ permissions: ["sales.opportunity.product.update"] })
);
