import type { DB } from "../generated/kysely-tailordb";
import {
  CostElementNotFoundError,
  CostObjectNotFoundError,
  AccountingPeriodNotFoundError,
} from "../lib/errors.generated";
import { sumPostedActualAmount } from "../lib/actualAmounts";
import { err, ok, type ReadonlyDB } from "@tailor-platform/erp-kit/module";

export interface CalculateBudgetAvailabilityInput {
  costObjectType: string;
  costObjectId: string;
  costElementId: string;
  accountingPeriodId: string;
}

/**
 * Function: calculateBudgetAvailability
 *
 * Computes budget availability for a given cost element, cost object, and
 * accounting period using the formula:
 * Available = Budget - (Actual + Commitments + Encumbrances)
 *
 * Encumbrances are reserved for future extension (treated as 0).
 */
export async function run(db: ReadonlyDB<DB>, input: CalculateBudgetAvailabilityInput) {
  // Step 1: Validate cost element exists
  const costElement = await db
    .selectFrom("CostElement")
    .selectAll()
    .where("id", "=", input.costElementId)
    .executeTakeFirst();

  if (!costElement) {
    return err(new CostElementNotFoundError(input.costElementId));
  }

  // Step 2: Validate cost object exists (polymorphic: CostCenter, ProfitCenter, or InternalOrder)
  let costObject: unknown;
  if (input.costObjectType === "CostCenter") {
    costObject = await db
      .selectFrom("CostCenter")
      .selectAll()
      .where("id", "=", input.costObjectId)
      .executeTakeFirst();
  } else if (input.costObjectType === "ProfitCenter") {
    costObject = await db
      .selectFrom("ProfitCenter")
      .selectAll()
      .where("id", "=", input.costObjectId)
      .executeTakeFirst();
  } else if (input.costObjectType === "InternalOrder") {
    costObject = await db
      .selectFrom("InternalOrder")
      .selectAll()
      .where("id", "=", input.costObjectId)
      .executeTakeFirst();
  }

  if (!costObject) {
    return err(new CostObjectNotFoundError(input.costObjectId));
  }

  // Step 3: Validate accounting period exists
  const accountingPeriod = await db
    .selectFrom("AccountingPeriod")
    .selectAll()
    .where("id", "=", input.accountingPeriodId)
    .executeTakeFirst();

  if (!accountingPeriod) {
    return err(new AccountingPeriodNotFoundError(input.accountingPeriodId));
  }

  // Step 4: Find the ACTIVE budget for the given cost object and fiscal year
  // where the plan version type is BUDGET
  const budget = await db
    .selectFrom("Budget")
    .innerJoin("PlanVersion", "PlanVersion.id", "Budget.planVersionId")
    .select(["Budget.id"])
    .where("Budget.costObjectType", "=", input.costObjectType)
    .where("Budget.costObjectId", "=", input.costObjectId)
    .where("Budget.fiscalYearId", "=", accountingPeriod.fiscalYearId)
    .where("Budget.status", "=", "ACTIVE")
    .where("PlanVersion.type", "=", "BUDGET")
    .executeTakeFirst();

  // Step 5: If no active budget found, return null availability
  if (!budget) {
    return ok({ availability: null, reason: "NO_BUDGET_DEFINED" });
  }

  // Step 6: Find the budget line item matching the cost element and period
  const lineItem = await db
    .selectFrom("BudgetLineItem")
    .selectAll()
    .where("budgetId", "=", budget.id)
    .where("costElementId", "=", input.costElementId)
    .where("accountingPeriodId", "=", input.accountingPeriodId)
    .executeTakeFirst();

  // Step 7: If no matching line item, return null availability
  if (!lineItem) {
    return ok({ availability: null, reason: "NO_BUDGET_LINE_ITEM" });
  }

  // Step 8: Get the planned amount from the budget line item
  const plannedAmount = lineItem.plannedAmount;

  // Step 9: Sum commitment amounts for the same cost element, cost object, and
  // period where status is not "RELEASED"
  const commitmentResult = await db
    .selectFrom("Commitment")
    .select((eb) => eb.fn.sum<number>("amount").as("totalCommitment"))
    .where("costElementId", "=", input.costElementId)
    .where("costObjectType", "=", input.costObjectType)
    .where("costObjectId", "=", input.costObjectId)
    .where("accountingPeriodId", "=", input.accountingPeriodId)
    .where("status", "!=", "RELEASED")
    .executeTakeFirst();

  const commitmentAmount = commitmentResult?.totalCommitment ?? 0;

  // Step 10: Sum posted actual amounts from finance-ledger journal lines
  const actualAmount = await sumPostedActualAmount(db, {
    accountingPeriodId: input.accountingPeriodId,
    costObjectType: input.costObjectType,
    costObjectId: input.costObjectId,
    costElementId: input.costElementId,
  });

  // Step 11: Encumbrance amount is zero (future extension placeholder)
  const encumbranceAmount = 0;

  // Step 12: Compute available = plannedAmount - (actualAmount + commitmentAmount + encumbranceAmount)
  const availableAmount = plannedAmount - (actualAmount + commitmentAmount + encumbranceAmount);

  return ok({
    availability: {
      plannedAmount,
      actualAmount,
      commitmentAmount,
      encumbranceAmount,
      availableAmount,
    },
  });
}
