import { Collection } from "@discordjs/collection";
import { connectWiseApi } from "../../../constants";
import {
  CWOpportunity,
  CWOpportunityCreate,
  CWOpportunitySummary,
  CWForecast,
  CWForecastItem,
  CWOpportunityProduct,
  CWForecastItemCreate,
  CWProcurementProduct,
  CWProcurementProductCreate,
  CWOpportunityNote,
  CWOpportunityNoteCreate,
  CWOpportunityNoteUpdate,
  CWOpportunityContact,
  CWOpportunityUpdate,
} from "./opportunity.types";

export const opportunityCw = {
  /**
   * Count Opportunities
   *
   * Returns the total number of opportunities in ConnectWise.
   * Optionally accepts CW conditions string for filtered counts.
   */
  countItems: async (conditions?: string): Promise<number> => {
    const query = conditions
      ? `/sales/opportunities/count?conditions=${encodeURIComponent(conditions)}`
      : "/sales/opportunities/count";
    const response = await connectWiseApi.get(query);
    return response.data.count;
  },

  /**
   * Fetch All Opportunity Summaries
   *
   * Lightweight fetch returning only id and _info (for lastUpdated comparison).
   * Paginates through all opportunities.
   */
  fetchAllSummaries: async (): Promise<
    Collection<number, CWOpportunitySummary>
  > => {
    const allItems = new Collection<number, CWOpportunitySummary>();
    const pageSize = 1000;

    const count = await opportunityCw.countItems();
    const totalPages = Math.ceil(count / pageSize);

    for (let page = 0; page < totalPages; page++) {
      const response = await connectWiseApi.get(
        `/sales/opportunities?page=${page + 1}&pageSize=${pageSize}&fields=id,_info`,
      );
      const items: CWOpportunitySummary[] = response.data;

      for (const item of items) {
        allItems.set(item.id, item);
      }
    }

    return allItems;
  },

  /**
   * Fetch All Opportunities (Full)
   *
   * Fetches all opportunities with complete data. Paginates through
   * the full list.
   */
  fetchAll: async (
    conditions?: string,
  ): Promise<Collection<number, CWOpportunity>> => {
    const allItems = new Collection<number, CWOpportunity>();
    const pageSize = 1000;

    const count = await opportunityCw.countItems(conditions);
    const totalPages = Math.ceil(count / pageSize);

    for (let page = 0; page < totalPages; page++) {
      const conditionsParam = conditions
        ? `&conditions=${encodeURIComponent(conditions)}`
        : "";
      const response = await connectWiseApi.get(
        `/sales/opportunities?page=${page + 1}&pageSize=${pageSize}${conditionsParam}`,
      );
      const items: CWOpportunity[] = response.data;

      for (const item of items) {
        allItems.set(item.id, item);
      }
    }

    return allItems;
  },

  /**
   * Fetch Single Opportunity
   *
   * Fetches a single opportunity by its ConnectWise ID.
   */
  fetch: async (id: number): Promise<CWOpportunity> => {
    const response = await connectWiseApi.get(`/sales/opportunities/${id}`);
    return response.data;
  },

  /**
   * Create Opportunity
   *
   * Creates a new opportunity in ConnectWise via POST.
   * Strips null/undefined values from the payload — CW rejects
   * null reference objects on create; omitting them lets CW apply
   * its own defaults.
   */
  create: async (data: CWOpportunityCreate): Promise<CWOpportunity> => {
    const cleaned = Object.fromEntries(
      Object.entries(data).filter(([, v]) => v != null),
    );
    const response = await connectWiseApi.post("/sales/opportunities", cleaned);
    return response.data;
  },

  /**
   * Update Opportunity
   *
   * Applies a JSON Patch update to an opportunity record in ConnectWise.
   * Each key in `data` produces a replace operation.
   */
  update: async (
    opportunityId: number,
    data: CWOpportunityUpdate,
  ): Promise<CWOpportunity> => {
    const operations = Object.entries(data).map(([key, value]) => ({
      op: "replace" as const,
      path: key,
      value,
    }));

    const response = await connectWiseApi.patch(
      `/sales/opportunities/${opportunityId}`,
      operations,
    );
    return response.data;
  },

  /**
   * Fetch Opportunities by Company
   *
   * Fetches all opportunities associated with a specific ConnectWise company ID.
   */
  fetchByCompany: async (
    cwCompanyId: number,
  ): Promise<Collection<number, CWOpportunity>> => {
    return opportunityCw.fetchAll(`company/id=${cwCompanyId}`);
  },

  /**
   * Fetch Opportunity Products
   *
   * Fetches the full forecast object (products, revenue summaries, totals)
   * for a given opportunity.
   */
  fetchProducts: async (opportunityId: number): Promise<CWOpportunityProduct[]> => {
    const response = await connectWiseApi.get(
      `/procurement/products?conditions=opportunity/id=${opportunityId}&pageSize=1000`,
    );
    return response.data;
  },

  /**
   * Create Forecast Items
   *
   * Adds one or more forecast items (products) to an opportunity using
   * POST. The CW forecast endpoint expects a Forecast object with a
   * `forecastItems` array — we wrap just the new items inside that
   * structure so existing items are never sent or touched.
   */
  createProducts: async (
    opportunityId: number,
    data: CWForecastItemCreate | CWForecastItemCreate[],
  ): Promise<CWForecastItem[]> => {
    const items_to_add = Array.isArray(data) ? data : [data];
    const url = `/sales/opportunities/${opportunityId}/forecast`;

    // 1. Fetch existing products to derive defaults & diff IDs later
    const existing = await opportunityCw.fetchProducts(opportunityId);
    const existingIds = new Set(
      existing.map((p) => p.forecastDetailId).filter((id): id is number => id != null),
    );

    // Derive sensible defaults from an existing item when available
    const templateItem = existing[0];
    const defaultStatus = templateItem?.forecastStatus
      ? { id: templateItem.forecastStatus.id }
      : { id: 1 };
    const defaultForecastType = "Product";

    // 2. Build forecast items with required CW fields filled in
    const forecastItems = items_to_add.map((newItem) => ({
      opportunity: { id: opportunityId },
      status: defaultStatus,
      forecastType: defaultForecastType,
      ...(newItem as Record<string, unknown>),
    }));

    // 3. POST a Forecast wrapper containing only the new items
    const response = await connectWiseApi.post(url, { forecastItems });
    const updatedForecast: CWForecast = response.data;

    // 4. Find newly-created item(s) by diffing IDs
    const newItems = (updatedForecast.forecastItems ?? []).filter(
      (fi) => !existingIds.has(fi.id),
    );

    // Fall back to the last N items if ID diffing finds nothing
    return newItems.length > 0
      ? newItems
      : (updatedForecast.forecastItems ?? []).slice(-items_to_add.length);
  },

  /**
   * Update Forecast Item
   *
   * PATCHes a single forecast item on the parent `/forecast` endpoint.
   * CW supports JSON Patch with paths like `/forecastItems/{index}/field`.
   * This preserves item IDs (unlike PUT which always regenerates them)
   * and does NOT recalculate revenue/cost from linked catalog items.
   *
   * NOTE: Not all fields are patchable — `sequenceNumber` and `quantity`
   * are read-only on forecast items. Product ordering is managed locally
   * via `OpportunityController.resequenceProducts()` and stored in the
   * database `productSequence` field.
   */
  updateProduct: async (
    opportunityId: number,
    forecastItemId: number,
    data: Record<string, unknown>,
  ): Promise<CWForecastItem> => {
    const items = await opportunityCw.fetchProducts(opportunityId);
    const idx = items.findIndex((p) => p.forecastDetailId === forecastItemId);
    if (idx === -1) {
      throw new Error(
        `Forecast item ${forecastItemId} not found on opportunity ${opportunityId}`,
      );
    }

    const operations = Object.entries(data).map(([key, value]) => ({
      op: "replace" as const,
      path: `/forecastItems/${idx}/${key}`,
      value,
    }));

    const url = `/sales/opportunities/${opportunityId}/forecast`;
    const response = await connectWiseApi.patch(url, operations);
    const updated: CWForecast = response.data;
    return (updated.forecastItems ?? [])[idx]!;
  },

  /**
   * Bulk-update Forecast Items
   *
   * PATCHes multiple forecast items in a single request via the parent
   * `/forecast` endpoint. All patch operations are sent in one array.
   */
  bulkUpdateProducts: async (
    opportunityId: number,
    updates: Map<number, Record<string, unknown>>,
  ): Promise<CWForecastItem[]> => {
    const items = await opportunityCw.fetchProducts(opportunityId);

    const operations: { op: "replace"; path: string; value: unknown }[] = [];
    const touchedIndices: number[] = [];

    for (const [itemId, changes] of updates) {
      const idx = items.findIndex((p) => p.forecastDetailId === itemId);
      if (idx === -1) {
        throw new Error(
          `Forecast item ${itemId} not found on opportunity ${opportunityId}`,
        );
      }
      touchedIndices.push(idx);
      for (const [key, value] of Object.entries(changes)) {
        operations.push({
          op: "replace",
          path: `/forecastItems/${idx}/${key}`,
          value,
        });
      }
    }

    const url = `/sales/opportunities/${opportunityId}/forecast`;
    const response = await connectWiseApi.patch(url, operations);
    const updated: CWForecast = response.data;

    return touchedIndices.map((i) => (updated.forecastItems ?? [])[i]!);
  },

  /**
   * Delete Forecast Item
   *
   * Removes a forecast item from an opportunity by PUTting the forecast
   * without the target item. CW's forecast endpoint replaces the entire
   * forecast items list on PUT.
   */
  deleteProduct: async (
    opportunityId: number,
    productId: number,
  ): Promise<void> => {
    const products = await opportunityCw.fetchProducts(opportunityId);
    const found = products.find((p) => p.id === productId);
    if (!found) {
      throw new Error(
        `Product ${productId} not found on opportunity ${opportunityId}`,
      );
    }
    const filtered = products.filter((p) => p.id !== productId);
    const url = `/procurement/products/${productId}`;
    await connectWiseApi.delete(url);
  },

  /**
   * Fetch Opportunity Notes
   *
   * Fetches notes associated with a given opportunity.
   */
  fetchNotes: async (opportunityId: number): Promise<CWOpportunityNote[]> => {
    const response = await connectWiseApi.get(
      `/sales/opportunities/${opportunityId}/notes`,
    );
    return response.data;
  },

  /**
   * Fetch Single Note
   *
   * Fetches a single note by its ID on the given opportunity.
   */
  fetchNote: async (
    opportunityId: number,
    noteId: number,
  ): Promise<CWOpportunityNote> => {
    const response = await connectWiseApi.get(
      `/sales/opportunities/${opportunityId}/notes/${noteId}`,
    );
    return response.data;
  },

  /**
   * Create Note
   *
   * Creates a new note on the given opportunity.
   */
  createNote: async (
    opportunityId: number,
    data: CWOpportunityNoteCreate,
  ): Promise<CWOpportunityNote> => {
    const response = await connectWiseApi.post(
      `/sales/opportunities/${opportunityId}/notes`,
      data,
    );
    return response.data;
  },

  /**
   * Update Note
   *
   * Updates an existing note on the given opportunity.
   */
  updateNote: async (
    opportunityId: number,
    noteId: number,
    data: CWOpportunityNoteUpdate,
  ): Promise<CWOpportunityNote> => {
    const response = await connectWiseApi.patch(
      `/sales/opportunities/${opportunityId}/notes/${noteId}`,
      Object.entries(data).map(([key, value]) => ({
        op: "replace",
        path: key,
        value,
      })),
    );
    return response.data;
  },

  /**
   * Delete Note
   *
   * Deletes a note from the given opportunity.
   */
  deleteNote: async (opportunityId: number, noteId: number): Promise<void> => {
    await connectWiseApi.delete(
      `/sales/opportunities/${opportunityId}/notes/${noteId}`,
    );
  },

  /**
   * Fetch Opportunity Contacts
   *
   * Fetches contacts associated with a given opportunity.
   */
  fetchContacts: async (
    opportunityId: number,
  ): Promise<CWOpportunityContact[]> => {
    const response = await connectWiseApi.get(
      `/sales/opportunities/${opportunityId}/contacts`,
    );
    return response.data;
  },

  /**
   * Fetch Procurement Products
   *
   * Fetches procurement product records linked to an opportunity.
   * These contain cancellation data (cancelledFlag, cancelledReason, etc.)
   * that the forecast endpoint does not provide.
   */
  fetchProcurementProducts: async (
    opportunityId: number,
  ): Promise<Record<string, unknown>[]> => {
    const response = await connectWiseApi.get(
      `/procurement/products?conditions=${encodeURIComponent(`opportunity/id=${opportunityId}`)}&fields=id,forecastDetailId,cancelledFlag,quantityCancelled,cancelledReason,cancelledBy,cancelledDate,customFields`,
    );
    return response.data;
  },

  /**
   * Create Procurement Products
   *
   * Creates one or more procurement products linked to an opportunity.
   * This endpoint supports procurement customFields (unlike forecast items).
   */
  createProcurementProducts: async (
    data: CWProcurementProductCreate | CWProcurementProductCreate[],
  ): Promise<CWProcurementProduct[]> => {
    const productsToCreate = Array.isArray(data) ? data : [data];
    const created: CWProcurementProduct[] = [];

    for (const product of productsToCreate) {
      const response = await connectWiseApi.post(
        `/procurement/products`,
        product,
      );
      created.push(response.data as CWProcurementProduct);
    }

    return created;
  },

  /**
   * Fetch Procurement Product by Forecast Detail
   *
   * Finds the procurement product linked to a given forecast item ID
   * on an opportunity.
   */
  fetchProcurementProductByForecastDetail: async (
    opportunityId: number,
    forecastDetailId: number,
  ): Promise<CWProcurementProduct | null> => {
    const conditions = `opportunity/id=${opportunityId} and forecastDetailId=${forecastDetailId}`;
    const response = await connectWiseApi.get(
      `/procurement/products?conditions=${encodeURIComponent(conditions)}&fields=id,forecastDetailId,description,customerDescription,quantity,price,cost,taxableFlag,specialOrderFlag,customFields`,
    );

    const items = (response.data ?? []) as CWProcurementProduct[];
    return items[0] ?? null;
  },

  /**
   * Update Procurement Product
   *
   * Applies a JSON Patch update to a procurement product record.
   */
  updateProcurementProduct: async (
    procurementProductId: number,
    data: Record<string, unknown>,
  ): Promise<CWProcurementProduct> => {
    const operations = Object.entries(data).map(([key, value]) => ({
      op: "replace" as const,
      path: key,
      value,
    }));

    const response = await connectWiseApi.patch(
      `/procurement/products/${procurementProductId}`,
      operations,
    );
    return response.data as CWProcurementProduct;
  },

  /**
   * Delete Opportunity
   *
   * Deletes an opportunity from ConnectWise by its CW opportunity ID.
   */
  delete: async (opportunityId: number): Promise<void> => {
    await connectWiseApi.delete(`/sales/opportunities/${opportunityId}`);
  },

  /**
   * Fetch Opportunities from ConnectWise API
   * Depends on pagination from the CW API.
   */
};
