import {
  CatalogItem,
  CatalogCategory,
  CatalogSubcategory,
  CatalogManufacturer,
} from "../../generated/prisma/client";
import { prisma } from "../constants";
import { CatalogItem as CWCatalogItem } from "../modules/cw-utils/procurement/catalog.types";
import GenericError from "../Errors/GenericError";

/**
 * Shape of the Prisma result when catalog items are queried with
 * `include: { linkedItems, manufacturer, subcategory: { include: { category } } }`.
 */
type CatalogItemWithRelations = CatalogItem & {
  linkedItems?: (CatalogItem & {
    manufacturer?: CatalogManufacturer | null;
    subcategory?: CatalogSubcategory & { category?: CatalogCategory | null };
  })[];
  manufacturer?: CatalogManufacturer | null;
  subcategory?: (CatalogSubcategory & { category?: CatalogCategory | null }) | null;
};

/**
 * Catalog Item Controller
 *
 * This class encapsulates a catalog item entity and provides domain methods
 * for accessing, refreshing, and serializing catalog item data. It bridges
 * the internal database representation with ConnectWise catalog data.
 */
export class CatalogItemController {
  /** The ConnectWise catalog record ID (`CatalogItem.id` in Prisma — Int @unique). */
  public readonly cwCatalogId: number;
  /** The Prisma primary key (UUID). */
  public readonly id: string;
  public name: string;
  public description: string | null;
  public customerDescription: string | null;
  public internalNotes: string | null;

  public readonly identifier: string | null;

  public category: string | null;
  public categoryCwId: number | null;
  public subcategory: string | null;
  public subcategoryCwId: number | null;

  public manufacturer: string | null;
  public manufactureCwId: number | null;
  public partNumber: string | null;

  public vendorName: string | null;
  public vendorSku: string | null;
  public vendorCwId: number | null;

  public price: number;
  public cost: number;

  public inactive: boolean;
  public salesTaxable: boolean;

  public onHand: number;
  public cwLastUpdated: Date | null;

  private _linkedItems: CatalogItemController[];

  public readonly createdAt: Date;
  public updatedAt: Date;

  constructor(itemData: CatalogItemWithRelations) {
    // Support both new relational schema (numeric id + uid) and old-style flat
    // mock data (string id, no uid). When `id` is a string, treat it as the
    // internal string uid; when it's a number, it's the CW catalog ID.
    const idVal = (itemData as any).id;
    if (typeof idVal === "number") {
      this.cwCatalogId = idVal;
      this.id = (itemData as any).uid ?? String(idVal);
    } else {
      // Legacy / flat mock: string id is the internal uid
      this.cwCatalogId = (itemData as any).cwCatalogId ?? 0;
      this.id = String(idVal ?? "");
    }

    this.name = itemData.name;
    this.description = itemData.description;
    this.customerDescription = itemData.customerDescription;
    this.internalNotes = itemData.internalNotes;
    this.identifier = itemData.identifier;

    // Extract category/subcategory from relation object (Prisma schema format)
    const sub = (itemData as any).subcategory;
    const subIsObj = sub != null && typeof sub === "object";
    const cat = subIsObj ? sub?.category : null;
    const mfr = (itemData as any).manufacturer;
    const mfrIsObj = mfr != null && typeof mfr === "object";

    this.category = cat?.name ?? null;
    this.categoryCwId = cat?.id ?? null;
    this.subcategory = subIsObj ? sub?.name ?? null : null;
    this.subcategoryCwId = subIsObj ? sub?.id ?? null : null;
    this.manufacturer = mfrIsObj ? mfr?.name ?? null : null;
    this.manufactureCwId = mfrIsObj ? mfr?.id ?? null : null;

    this.partNumber = itemData.partNumber;
    this.vendorName = itemData.vendorName;
    this.vendorSku = itemData.vendorSku;
    this.vendorCwId = itemData.vendorCwId;
    this.price = itemData.price;
    this.cost = itemData.cost;
    this.inactive = itemData.inactive;
    this.salesTaxable = itemData.salesTaxable;
    this.onHand = itemData.onHand;
    this.cwLastUpdated = itemData.cwLastUpdated;
    this.createdAt = itemData.createdAt;
    this.updatedAt = itemData.updatedAt;

    this._linkedItems = (itemData.linkedItems ?? []).map(
      (linked) => new CatalogItemController(linked),
    );
  }

  /**
   * Refresh Inventory
   *
   * Fetches the latest on-hand inventory count from ConnectWise
   * and updates both the controller state and the database.
   *
   * @returns {Promise<CatalogItemController>} - The updated controller
   */
  public async refreshInventory(): Promise<CatalogItemController> {
    const result = await prisma.productInventory.aggregate({
      where: { itemId: this.cwCatalogId },
      _sum: { qtyOnHand: true },
    });
    const onHand = result._sum.qtyOnHand ?? 0;

    if (onHand !== this.onHand) {
      await prisma.catalogItem.update({
        where: { uid: this.id },
        data: { onHand },
      });
      this.onHand = onHand;
    }

    return this;
  }

  /**
   * Fetch Linked Items
   *
   * Returns the linked catalog items as an array of controllers.
   *
   * @returns {CatalogItemController[]} - Array of linked item controllers
   */
  public getLinkedItems(): CatalogItemController[] {
    return this._linkedItems;
  }

  /**
   * Link Item
   *
   * Links another catalog item to this item. The relationship is bidirectional
   * via the Prisma implicit many-to-many.
   *
   * @param targetId - The internal ID of the catalog item to link
   * @returns {Promise<CatalogItemController>} - The updated controller
   */
  public async linkItem(targetId: string): Promise<CatalogItemController> {
    if (targetId === this.id) {
      throw new GenericError({
        message: "Cannot link a catalog item to itself",
        name: "InvalidLinkTarget",
        cause: `Item '${this.id}' cannot be linked to itself`,
        status: 400,
      });
    }

    const target = await prisma.catalogItem.findFirst({
      where: { uid: targetId },
    });

    if (!target) {
      throw new GenericError({
        message: "Target catalog item not found",
        name: "CatalogItemNotFound",
        cause: `No catalog item exists with ID '${targetId}'`,
        status: 404,
      });
    }

    const updated = await prisma.catalogItem.update({
      where: { uid: this.id },
      data: {
        linkedItems: { connect: { uid: targetId } },
      },
      include: { linkedItems: true },
    });

    this._linkedItems = (updated.linkedItems ?? []).map(
      (linked) => new CatalogItemController(linked),
    );

    return this;
  }

  /**
   * Unlink Item
   *
   * Removes the link between this catalog item and another.
   *
   * @param targetId - The internal ID of the catalog item to unlink
   * @returns {Promise<CatalogItemController>} - The updated controller
   */
  public async unlinkItem(targetId: string): Promise<CatalogItemController> {
    const updated = await prisma.catalogItem.update({
      where: { uid: this.id },
      data: {
        linkedItems: { disconnect: { uid: targetId } },
      },
      include: { linkedItems: true },
    });

    this._linkedItems = (updated.linkedItems ?? []).map(
      (linked) => new CatalogItemController(linked),
    );

    return this;
  }

  /**
   * To JSON
   *
   * Serializes the catalog item into a safe, API-friendly object.
   *
   * @param opts - Options to control output
   * @returns - A JSON-safe representation of the catalog item
   */
  public toJson(opts?: { includeLinkedItems?: boolean }): Record<string, any> {
    return {
      id: this.id,
      cwCatalogId: this.cwCatalogId,
      identifier: this.identifier,
      name: this.name,
      description: this.description,
      customerDescription: this.customerDescription,
      internalNotes: this.internalNotes,
      category: this.category,
      categoryCwId: this.categoryCwId,
      subcategory: this.subcategory,
      subcategoryCwId: this.subcategoryCwId,
      manufacturer: this.manufacturer,
      manufactureCwId: this.manufactureCwId,
      partNumber: this.partNumber,
      vendorName: this.vendorName,
      vendorSku: this.vendorSku,
      vendorCwId: this.vendorCwId,
      price: this.price,
      cost: this.cost,
      inactive: this.inactive,
      salesTaxable: this.salesTaxable,
      onHand: this.onHand,
      cwLastUpdated: this.cwLastUpdated,
      linkedItems: opts?.includeLinkedItems
        ? this._linkedItems.map((item) => item.toJson())
        : undefined,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
    };
  }
}
