import { ok, err, type ReadonlyDB } from "@tailor-platform/erp-kit/core";
import type { DB } from "../generated/kysely-tailordb";
import {
  BomReferenceRequiredError,
  EffectivityDateRequiredError,
  BomNotFoundError,
  BomNotEffectiveOnDateError,
  ChildBomNotResolvedError,
  BomCircularReferenceDetectedError,
} from "../lib/errors.generated";

export interface ExplodeBillOfMaterialInput {
  bomId: string;
  effectivityDate: Date;
}

interface ExplodedComponent {
  itemId: string;
  requiredQuantity: number;
  unitOfMeasure: string | null;
  depth: number;
  bomPath: string[];
  isSubassembly: boolean;
}

/**
 * Function: explodeBillOfMaterial
 *
 * Expands one BOM revision into recursive component requirements using the
 * requested effectivity date and site-applicable child BOM scope.
 */
export async function run(db: ReadonlyDB<DB>, input: ExplodeBillOfMaterialInput) {
  if (!input.bomId) {
    return err(new BomReferenceRequiredError("bomId"));
  }

  if (!input.effectivityDate) {
    return err(new EffectivityDateRequiredError("effectivityDate"));
  }

  const rootBom = await db
    .selectFrom("BillOfMaterial")
    .selectAll()
    .where("id", "=", input.bomId)
    .executeTakeFirst();

  if (!rootBom) {
    return err(new BomNotFoundError(input.bomId));
  }

  if (!isEffectiveOnDate(rootBom, input.effectivityDate)) {
    return err(new BomNotEffectiveOnDateError(input.bomId));
  }

  const components: ExplodedComponent[] = [];
  const visitedItems = new Set<string>();

  const result = await expandBom(
    db,
    rootBom.id,
    rootBom.companyId,
    rootBom.siteId,
    rootBom.bomType as string,
    input.effectivityDate,
    1, // multiplier
    0, // depth
    [rootBom.id],
    visitedItems,
    components,
  );

  if (result) {
    return result; // error result
  }

  return ok({ components });
}

function isEffectiveOnDate(
  bom: { effectivityStartDate: unknown; effectivityEndDate: unknown },
  date: Date,
): boolean {
  const dateMs = date.getTime();

  if (bom.effectivityStartDate) {
    const start = new Date(bom.effectivityStartDate as string);
    if (dateMs < start.getTime()) {
      return false;
    }
  }

  if (bom.effectivityEndDate) {
    const end = new Date(bom.effectivityEndDate as string);
    if (dateMs > end.getTime()) {
      return false;
    }
  }

  return true;
}

function resolveApplicableChildBom(
  childBoms: {
    id: string;
    siteId?: string | null;
    bomType: string;
    effectivityStartDate: unknown;
    effectivityEndDate: unknown;
  }[],
  effectivityDate: Date,
  applicableSiteId: string | null,
) {
  const effectiveBoms = childBoms.filter((bom) => {
    if (!isEffectiveOnDate(bom, effectivityDate)) {
      return false;
    }

    if (applicableSiteId == null) {
      return bom.siteId == null;
    }

    return bom.siteId == null || bom.siteId === applicableSiteId;
  });

  return (
    effectiveBoms.find((bom) => bom.siteId === applicableSiteId) ??
    effectiveBoms.find((bom) => bom.siteId == null)
  );
}

async function expandBom(
  db: ReadonlyDB<DB>,
  bomId: string,
  companyId: string,
  applicableSiteId: string | null,
  bomType: string,
  effectivityDate: Date,
  parentQuantityMultiplier: number,
  depth: number,
  bomPath: string[],
  visitedItems: Set<string>,
  components: ExplodedComponent[],
): Promise<{ ok: false; error: Error } | undefined> {
  const lines = await db
    .selectFrom("BillOfMaterialLine")
    .selectAll()
    .where("billOfMaterialId", "=", bomId)
    .execute();

  for (const line of lines) {
    const requiredQuantity = line.requiredQuantity * parentQuantityMultiplier;
    const isSubassembly = line.isSubassembly === true;

    // KIT: issue-only component demand — no recursive expansion
    if (bomType === "KIT") {
      components.push({
        itemId: line.itemId,
        requiredQuantity,
        unitOfMeasure: line.unitOfMeasure,
        depth: depth + 1,
        bomPath: [...bomPath],
        isSubassembly: false, // KIT components are always issue-only
      });
      continue;
    }

    // PHANTOM: flatten lines into parent path
    if (bomType === "PHANTOM") {
      if (isSubassembly) {
        // Resolve child BOM and expand at SAME depth (no depth increment)
        if (visitedItems.has(line.itemId)) {
          return err(new BomCircularReferenceDetectedError(line.itemId));
        }
        visitedItems.add(line.itemId);

        const childBoms = await db
          .selectFrom("BillOfMaterial")
          .selectAll()
          .where("parentItemId", "=", line.itemId)
          .where("companyId", "=", companyId)
          .where("status", "=", "ACTIVE")
          .execute();
        const effectiveChildBom = resolveApplicableChildBom(
          childBoms,
          effectivityDate,
          applicableSiteId,
        );
        if (!effectiveChildBom) {
          return err(new ChildBomNotResolvedError(line.itemId));
        }

        const childResult = await expandBom(
          db,
          effectiveChildBom.id,
          companyId,
          applicableSiteId,
          effectiveChildBom.bomType,
          effectivityDate,
          requiredQuantity,
          depth, // same depth — flattening
          [...bomPath, effectiveChildBom.id],
          visitedItems,
          components,
        );
        if (childResult) return childResult;
        visitedItems.delete(line.itemId);
      } else {
        // Non-subassembly in phantom: push at current depth (flattened)
        components.push({
          itemId: line.itemId,
          requiredQuantity,
          unitOfMeasure: line.unitOfMeasure,
          depth: depth + 1,
          bomPath: [...bomPath],
          isSubassembly: false,
        });
      }
      continue;
    }

    // MANUFACTURE (default): add the component and recurse if subassembly
    if (isSubassembly) {
      if (visitedItems.has(line.itemId)) {
        return err(new BomCircularReferenceDetectedError(line.itemId));
      }
      visitedItems.add(line.itemId);

      // Find an active child BOM for this item effective on the date
      const childBoms = await db
        .selectFrom("BillOfMaterial")
        .selectAll()
        .where("parentItemId", "=", line.itemId)
        .where("companyId", "=", companyId)
        .where("status", "=", "ACTIVE")
        .execute();

      const effectiveChildBom = resolveApplicableChildBom(
        childBoms,
        effectivityDate,
        applicableSiteId,
      );

      if (!effectiveChildBom) {
        return err(new ChildBomNotResolvedError(line.itemId));
      }

      const childBomType = effectiveChildBom.bomType;

      if (childBomType === "PHANTOM") {
        // PHANTOM child: skip the phantom item, flatten its children to current depth
        const childResult = await expandBom(
          db,
          effectiveChildBom.id,
          companyId,
          applicableSiteId,
          childBomType,
          effectivityDate,
          requiredQuantity,
          depth, // same depth — flattening
          [...bomPath, effectiveChildBom.id],
          visitedItems,
          components,
        );
        if (childResult) return childResult;
      } else {
        // Non-phantom child: push the subassembly and recurse deeper
        components.push({
          itemId: line.itemId,
          requiredQuantity,
          unitOfMeasure: line.unitOfMeasure,
          depth: depth + 1,
          bomPath: [...bomPath],
          isSubassembly: true,
        });

        const childResult = await expandBom(
          db,
          effectiveChildBom.id,
          companyId,
          applicableSiteId,
          childBomType,
          effectivityDate,
          requiredQuantity,
          depth + 1,
          [...bomPath, effectiveChildBom.id],
          visitedItems,
          components,
        );
        if (childResult) return childResult;
      }

      visitedItems.delete(line.itemId);
    } else {
      components.push({
        itemId: line.itemId,
        requiredQuantity,
        unitOfMeasure: line.unitOfMeasure,
        depth: depth + 1,
        bomPath: [...bomPath],
        isSubassembly: false,
      });
    }
  }

  return undefined; // success, no error
}
