import { prisma } from "../constants";
import { CatalogItemController } from "../controllers/CatalogItemController";
import GenericError from "../Errors/GenericError";
import {
  CATEGORY_TREE,
  getSubcategoriesForCategory,
  getSubcategoriesForGroup,
  ECOSYSTEM_TREE,
  isCategoryGroup,
} from "../modules/catalog-categories/catalogCategories";

/**
 * Standard include clause used by catalog item queries.
 * Includes one level of linked items plus the manufacturer and
 * subcategory (with its parent category) relations so the
 * CatalogItemController can populate all fields.
 */
const catalogItemInclude = {
  linkedItems: true,
  manufacturer: true,
  subcategory: { include: { category: true } },
} as const;

const LABOR_STYLE_CANDIDATES = {
  field: ["LABOR & INSTALLATION - FIELD", "LABOR - FIELD", "LABOR FIELD"],
  tech: ["LABOR & INSTALLATION - TECH", "LABOR - TECH", "LABOR TECH"],
} as const;

const DEFAULT_CATALOG_RPP = 30;

function normalizePositiveInt(value: number, fallback: number): number {
  const parsed = Number(value);
  if (!Number.isFinite(parsed) || parsed < 1) return fallback;
  return Math.floor(parsed);
}

function normalizeFiniteNumber(value?: number): number | undefined {
  if (value === undefined) return undefined;
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : undefined;
}

async function findCatalogByExactCandidates(
  candidates: readonly string[]
): Promise<CatalogItemController | null> {
  for (const candidate of candidates) {
    const item = await prisma.catalogItem.findFirst({
      where: {
        inactive: false,
        OR: [
          { identifier: { equals: candidate, mode: "insensitive" } },
          { name: { equals: candidate, mode: "insensitive" } },
        ],
      },
      include: catalogItemInclude,
    });

    if (item) return new CatalogItemController(item);
  }

  return null;
}

async function findCatalogByLaborStyle(
  style: "field" | "tech"
): Promise<CatalogItemController | null> {
  const fallback = await prisma.catalogItem.findFirst({
    where: {
      inactive: false,
      AND: [
        {
          OR: [
            { identifier: { contains: "labor", mode: "insensitive" } },
            { name: { contains: "labor", mode: "insensitive" } },
          ],
        },
        {
          OR: [
            { identifier: { contains: style, mode: "insensitive" } },
            { name: { contains: style, mode: "insensitive" } },
          ],
        },
      ],
    },
    include: catalogItemInclude,
    orderBy: { name: "asc" },
  });

  if (!fallback) return null;
  return new CatalogItemController(fallback);
}

/**
 * Filter options for catalog item queries.
 */
export interface CatalogFilterOpts {
  includeInactive?: boolean;
  category?: string;
  subcategory?: string;
  group?: string;
  manufacturer?: string;
  ecosystem?: string;
  inStock?: boolean;
  minPrice?: number;
  maxPrice?: number;
}

/**
 * Builds a Prisma `where` clause from filter options.
 */
function buildFilterWhere(opts: CatalogFilterOpts = {}) {
  const conditions: Record<string, unknown>[] = [];
  const minPrice = normalizeFiniteNumber(opts.minPrice);
  const maxPrice = normalizeFiniteNumber(opts.maxPrice);

  const parseNumericId = (value?: string): number | null => {
    if (!value) return null;
    if (!/^\d+$/.test(value)) return null;
    return Number(value);
  };

  const resolveCategoryNameById = (cwId: number): string | null => {
    return CATEGORY_TREE.find((c) => c.cwId === cwId)?.name ?? null;
  };

  const resolveSubcategoryNameById = (cwId: number): string | null => {
    for (const category of CATEGORY_TREE) {
      for (const entry of category.entries) {
        if (isCategoryGroup(entry)) {
          const child = entry.children.find((c) => c.cwId === cwId);
          if (child) return child.name;
          continue;
        }

        if (entry.cwId === cwId) return entry.name;
      }
    }
    return null;
  };

  const categoryId = parseNumericId(opts.category);
  const subcategoryId = parseNumericId(opts.subcategory);
  const resolvedCategoryName = categoryId
    ? resolveCategoryNameById(categoryId)
    : opts.category;

  if (!opts.includeInactive) {
    conditions.push({ inactive: false });
  }

  if (opts.category) {
    if (categoryId) {
      const categoryOr: Record<string, unknown>[] = [
        { subcategory: { is: { category: { is: { id: categoryId } } } } },
      ];
      if (resolvedCategoryName) {
        categoryOr.push({ subcategory: { is: { category: { is: { name: resolvedCategoryName } } } } });
      }
      conditions.push({ OR: categoryOr });
    } else {
      conditions.push({ subcategory: { is: { category: { is: { name: opts.category } } } } });
    }
  }

  if (opts.subcategory) {
    if (subcategoryId) {
      const resolvedSubcategoryName = resolveSubcategoryNameById(subcategoryId);
      const subcategoryOr: Record<string, unknown>[] = [
        { subcategory: { is: { id: subcategoryId } } },
      ];
      if (resolvedSubcategoryName) {
        subcategoryOr.push({ subcategory: { is: { name: resolvedSubcategoryName } } });
      }
      conditions.push({ OR: subcategoryOr });
    } else {
      conditions.push({ subcategory: { is: { name: opts.subcategory } } });
    }
  }

  if (opts.group && opts.category) {
    if (!resolvedCategoryName) {
      conditions.push({ subcategory: { is: { category: { is: { name: "__unknown_category__" } } } } });
    }
    if (resolvedCategoryName) {
      const subcats = getSubcategoriesForGroup(
        resolvedCategoryName,
        opts.group
      );
      if (subcats.length > 0) {
        conditions.push({ subcategory: { is: { name: { in: subcats } } } });
      }
    }
  } else if (opts.group && !opts.category) {
    // Try to find the group in any category
    for (const cat of CATEGORY_TREE) {
      const subcats = getSubcategoriesForGroup(cat.name, opts.group);
      if (subcats.length > 0) {
        conditions.push({
          subcategory: {
            is: {
              name: { in: subcats },
              category: { is: { name: cat.name } },
            },
          },
        });
        break;
      }
    }
  }

  if (opts.manufacturer) {
    conditions.push({
      manufacturer: {
        is: {
          name: { contains: opts.manufacturer, mode: "insensitive" },
        },
      },
    });
  }

  if (opts.ecosystem) {
    const eco = ECOSYSTEM_TREE.find(
      (e) => e.name.toLowerCase() === opts.ecosystem!.toLowerCase()
    );
    if (eco && eco.manufacturers.length > 0) {
      conditions.push({
        OR: eco.manufacturers.map((m) => ({
          manufacturer: {
            is: {
              name: { contains: m.name, mode: "insensitive" as const },
            },
          },
          subcategory: {
            is: {
              name: { startsWith: m.subcategoryPrefix },
              category: { is: { name: m.category } },
            },
          },
        })),
      });
    }
  }

  if (opts.inStock) {
    conditions.push({ onHand: { gt: 0 } });
  }

  if (minPrice !== undefined) {
    conditions.push({ price: { gte: minPrice } });
  }

  if (maxPrice !== undefined) {
    conditions.push({ price: { lte: maxPrice } });
  }

  return conditions.length > 0 ? { AND: conditions } : undefined;
}

export const procurement = {
  /**
   * Fetch Catalog Item
   *
   * Fetch a catalog item by its internal ID or ConnectWise catalog ID
   * and return a CatalogItemController instance.
   *
   * @param identifier - The internal ID (string) or CW catalog ID (number)
   * @returns {Promise<CatalogItemController>} - The catalog item controller
   */
  async fetchItem(identifier: string | number): Promise<CatalogItemController> {
    const isNumeric =
      typeof identifier === "number" || /^\d+$/.test(String(identifier));

    const item = await prisma.catalogItem.findFirst({
      where: isNumeric
        ? ({ cwCatalogId: Number(identifier) } as any)
        : {
            OR: [
              { uid: identifier as string },
              { identifier: identifier as string },
            ],
          },
      include: catalogItemInclude,
    });

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

    return new CatalogItemController(item);
  },

  /**
   * Fetch Labor Catalog Items
   *
   * Resolves canonical Field and Tech labor products from the local catalog.
   * Prefers exact identifier/name matches, then falls back to keyword matching.
   */
  async fetchLaborCatalogItems(): Promise<{
    field: CatalogItemController;
    tech: CatalogItemController;
  }> {
    const fieldItem =
      (await findCatalogByExactCandidates(LABOR_STYLE_CANDIDATES.field)) ??
      (await findCatalogByLaborStyle("field"));
    const techItem =
      (await findCatalogByExactCandidates(LABOR_STYLE_CANDIDATES.tech)) ??
      (await findCatalogByLaborStyle("tech"));

    if (!fieldItem || !techItem) {
      throw new GenericError({
        message: "Labor catalog products are not configured",
        name: "LaborCatalogProductsNotFound",
        cause:
          "Expected active FIELD and TECH labor catalog items in the local catalog",
        status: 500,
      });
    }

    return { field: fieldItem, tech: techItem };
  },

  /**
   * Fetch All Catalog Items (Paginated)
   *
   * Fetch pages of catalog items for pagination with optional filtering.
   *
   * @param page - Page number (1-based)
   * @param rpp  - Records per page
   * @param opts - Filter options
   * @returns {Promise<CatalogItemController[]>} - Array of catalog item controllers
   */
  async fetchPages(
    page: number,
    rpp: number,
    opts?: CatalogFilterOpts
  ): Promise<CatalogItemController[]> {
    const safePage = normalizePositiveInt(page, 1);
    const safeRpp = normalizePositiveInt(rpp, DEFAULT_CATALOG_RPP);
    const skip = (safePage - 1) * safeRpp;
    const take = safeRpp;

    const items = await prisma.catalogItem.findMany({
      where: buildFilterWhere(opts),
      skip,
      take,
      include: catalogItemInclude,
      orderBy: { name: "asc" },
    });

    return items.map((item) => new CatalogItemController(item));
  },

  /**
   * Search Catalog Items
   *
   * Search catalog items by name, description, part number, or vendor SKU
   * with pagination support and optional category/subcategory/ecosystem filters.
   *
   * @param query - Search query string
   * @param page  - Page number (1-based)
   * @param rpp   - Records per page
   * @param opts  - Filter options
   * @returns {Promise<CatalogItemController[]>} - Array of matching catalog item controllers
   */
  async search(
    query: string,
    page: number,
    rpp: number,
    opts?: CatalogFilterOpts
  ): Promise<CatalogItemController[]> {
    const safePage = normalizePositiveInt(page, 1);
    const safeRpp = normalizePositiveInt(rpp, DEFAULT_CATALOG_RPP);
    const skip = (safePage - 1) * safeRpp;
    const take = safeRpp;

    const filterWhere = buildFilterWhere(opts) ?? {};

    const items = await prisma.catalogItem.findMany({
      where: {
        ...filterWhere,
        OR: [
          { identifier: { contains: query, mode: "insensitive" } },
          { name: { contains: query, mode: "insensitive" } },
          { description: { contains: query, mode: "insensitive" } },
          { partNumber: { contains: query, mode: "insensitive" } },
          { vendorSku: { contains: query, mode: "insensitive" } },
          {
            manufacturer: {
              is: { name: { contains: query, mode: "insensitive" } },
            },
          },
        ],
      },
      skip,
      take,
      include: catalogItemInclude,
      orderBy: { name: "asc" },
    });

    return items.map((item) => new CatalogItemController(item));
  },

  /**
   * Count Catalog Items
   *
   * Returns the total number of catalog items matching the given filters.
   *
   * @param opts - Filter options
   * @returns {Promise<number>} - Total count
   */
  async count(
    opts?: CatalogFilterOpts & { activeOnly?: boolean }
  ): Promise<number> {
    // Support legacy `activeOnly` flag by mapping it to `includeInactive`
    const filterOpts: CatalogFilterOpts = {
      ...opts,
      includeInactive:
        opts?.includeInactive ?? (opts?.activeOnly ? false : true),
    };
    if (opts?.activeOnly) filterOpts.includeInactive = false;

    return prisma.catalogItem.count({
      where: buildFilterWhere(filterOpts),
    });
  },

  /**
   * Count Catalog Items (with search query)
   *
   * Returns the total number of catalog items matching a search query and filters.
   *
   * @param query - Search query string
   * @param opts  - Filter options
   * @returns {Promise<number>} - Total count
   */
  async countSearch(query: string, opts?: CatalogFilterOpts): Promise<number> {
    const filterWhere = buildFilterWhere(opts) ?? {};

    return prisma.catalogItem.count({
      where: {
        ...filterWhere,
        OR: [
          { identifier: { contains: query, mode: "insensitive" } },
          { name: { contains: query, mode: "insensitive" } },
          { description: { contains: query, mode: "insensitive" } },
          { partNumber: { contains: query, mode: "insensitive" } },
          { vendorSku: { contains: query, mode: "insensitive" } },
          {
            manufacturer: {
              is: { name: { contains: query, mode: "insensitive" } },
            },
          },
        ],
      },
    });
  },

  /**
   * Fetch Distinct Values
   *
   * Returns the distinct values for a given field across all catalog items.
   * Useful for populating filter dropdowns in the UI.
   *
   * @param field - The field to get distinct values for
   * @param opts  - Filter options to scope the distinct query
   * @returns {Promise<string[]>} - Sorted array of distinct non-null values
   */
  async fetchDistinctValues(
    field: "category" | "subcategory" | "manufacturer",
    opts?: CatalogFilterOpts
  ): Promise<string[]> {
    if (field === "manufacturer") {
      const items = await prisma.catalogItem.findMany({
        where: buildFilterWhere(opts),
        select: { manufacturer: { select: { name: true } } },
      }) as any[];
      const names = items
        .map((item) => item.manufacturer?.name ?? (typeof item.manufacturer === "string" ? item.manufacturer : null))
        .filter((v): v is string => v !== null);
      return [...new Set(names)].sort();
    }

    if (field === "subcategory") {
      const items = await prisma.catalogItem.findMany({
        where: buildFilterWhere(opts),
        select: { subcategory: { select: { name: true } } },
      }) as any[];
      const names = items
        .map((item) => item.subcategory?.name ?? (typeof item.subcategory === "string" ? item.subcategory : null))
        .filter((v): v is string => v !== null);
      return [...new Set(names)].sort();
    }

    // field === "category"
    const items = await prisma.catalogItem.findMany({
      where: buildFilterWhere(opts),
      select: { subcategory: { select: { category: { select: { name: true } } } } },
    }) as any[];
    const names = items
      .map((item) => item.subcategory?.category?.name ?? item.category ?? null)
      .filter((v): v is string => v !== null);
    return [...new Set(names)];
  },

  /**
   * Link Catalog Items
   *
   * Links a target catalog item to a source catalog item.
   *
   * @param sourceIdentifier - The source item's internal ID, identifier, or CW catalog ID
   * @param targetIdentifier - The target item's internal ID, identifier, or CW catalog ID
   * @returns {Promise<CatalogItemController>} - The updated source controller with linked items
   */
  async linkItems(
    sourceIdentifier: string | number,
    targetIdentifier: string | number
  ): Promise<CatalogItemController> {
    const source = await procurement.fetchItem(sourceIdentifier);
    const target = await procurement.fetchItem(targetIdentifier);

    return source.linkItem(target.id);
  },

  /**
   * Unlink Catalog Items
   *
   * Removes the link between a source catalog item and a target catalog item.
   *
   * @param sourceIdentifier - The source item's internal ID, identifier, or CW catalog ID
   * @param targetIdentifier - The target item's internal ID, identifier, or CW catalog ID
   * @returns {Promise<CatalogItemController>} - The updated source controller
   */
  async unlinkItems(
    sourceIdentifier: string | number,
    targetIdentifier: string | number
  ): Promise<CatalogItemController> {
    const source = await procurement.fetchItem(sourceIdentifier);
    const target = await procurement.fetchItem(targetIdentifier);

    return source.unlinkItem(target.id);
  },
};
