import type { Transaction } from "../generated/kysely-tailordb";
import {
  CompanyNotFoundError,
  AccountingPeriodNotFoundError,
  CostObjectNotFoundError,
  NoBudgetDataError,
} from "../lib/errors.generated";
import { listPostedActualAmountsByCostElement } from "../lib/actualAmounts";
import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/module";

export interface GenerateVarianceReportInput {
  companyId: string;
  accountingPeriodId: string;
  costObjectType: "CostCenter" | "ProfitCenter" | "InternalOrder";
  costObjectId: string;
}

export async function run(
  db: Transaction,
  input: GenerateVarianceReportInput,
  ctx: CommandContext,
) {
  void ctx;

  // Validate company exists and is ACTIVE
  const company = await db
    .selectFrom("Company" as never)
    .where("id" as never, "=", input.companyId as never)
    .selectAll()
    .executeTakeFirst();

  if (!company || (company as Record<string, unknown>).status !== "ACTIVE") {
    return err(new CompanyNotFoundError(input.companyId));
  }

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

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

  // Validate cost object exists (polymorphic lookup)
  const costObjectTable = input.costObjectType;
  const costObject = await db
    .selectFrom(costObjectTable as never)
    .where("id" as never, "=", input.costObjectId as never)
    .selectAll()
    .executeTakeFirst();

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

  // Look up budget line items for the cost object and period
  // Budget line items are linked through Budget -> BudgetLineItem
  // We need budgets for this cost object, then their line items for this period
  const budgetLineItems = await db
    .selectFrom("BudgetLineItem")
    .innerJoin("Budget", "Budget.id", "BudgetLineItem.budgetId")
    .where("Budget.costObjectType", "=", input.costObjectType)
    .where("Budget.costObjectId", "=", input.costObjectId)
    .where("BudgetLineItem.accountingPeriodId", "=", input.accountingPeriodId)
    .selectAll()
    .execute();

  if (!budgetLineItems || budgetLineItems.length === 0) {
    return err(new NoBudgetDataError(input.costObjectId));
  }

  // Look up cost elements for each budget line item to determine variance direction
  const actualAmountsByCostElement = await listPostedActualAmountsByCostElement(db, {
    accountingPeriodId: input.accountingPeriodId,
    costObjectType: input.costObjectType,
    costObjectId: input.costObjectId,
  });

  const plannedAmountsByCostElement = new Map<string, number>();
  for (const budgetLineItem of budgetLineItems) {
    plannedAmountsByCostElement.set(
      budgetLineItem.costElementId,
      (plannedAmountsByCostElement.get(budgetLineItem.costElementId) ?? 0) +
        budgetLineItem.plannedAmount,
    );
  }

  const costElementIds = new Set<string>([
    ...plannedAmountsByCostElement.keys(),
    ...actualAmountsByCostElement.keys(),
  ]);

  const lineData: {
    costElementId: string;
    plannedAmount: number;
    actualAmount: number;
    varianceAmount: number;
    variancePercentage: number;
    varianceDirection: "FAVORABLE" | "UNFAVORABLE";
  }[] = [];

  for (const costElementId of costElementIds) {
    const plannedAmount = plannedAmountsByCostElement.get(costElementId) ?? 0;
    const actualAmount = actualAmountsByCostElement.get(costElementId) ?? 0;

    const varianceAmount = actualAmount - plannedAmount;
    const variancePercentage =
      plannedAmount !== 0 ? ((actualAmount - plannedAmount) / plannedAmount) * 100 : 0;

    // Look up cost element to determine category for variance direction
    const costElement = await db
      .selectFrom("CostElement")
      .where("id" as never, "=", costElementId as never)
      .selectAll()
      .executeTakeFirst();

    const category =
      typeof (costElement as Record<string, unknown> | undefined)?.category === "string"
        ? ((costElement as Record<string, unknown>).category as string)
        : "overhead";

    let varianceDirection: "FAVORABLE" | "UNFAVORABLE";
    if (actualAmount === plannedAmount) {
      varianceDirection = "FAVORABLE";
    } else if (category === "revenue") {
      if (actualAmount > plannedAmount) {
        varianceDirection = "FAVORABLE";
      } else {
        varianceDirection = "UNFAVORABLE";
      }
    } else if (actualAmount < plannedAmount) {
      varianceDirection = "FAVORABLE";
    } else {
      varianceDirection = "UNFAVORABLE";
    }

    lineData.push({
      costElementId,
      plannedAmount,
      actualAmount,
      varianceAmount,
      variancePercentage,
      varianceDirection,
    });
  }

  // Compute totals
  const totalPlanned = lineData.reduce((sum, l) => sum + l.plannedAmount, 0);
  const totalActual = lineData.reduce((sum, l) => sum + l.actualAmount, 0);
  const totalVariance = lineData.reduce((sum, l) => sum + l.varianceAmount, 0);

  // Check for existing DRAFT report for same cost object + period - if exists, delete it and its lines
  const existingDraft = (await db
    .selectFrom("VarianceReport")
    .where("costObjectType" as never, "=", input.costObjectType as never)
    .where("costObjectId" as never, "=", input.costObjectId as never)
    .where("accountingPeriodId" as never, "=", input.accountingPeriodId as never)
    .where("status" as never, "=", "DRAFT" as never)
    .selectAll()
    .forUpdate()
    .executeTakeFirst()) as Record<string, unknown> | undefined;

  if (existingDraft) {
    // Delete existing lines
    await db
      .deleteFrom("VarianceReportLine" as never)
      .where("varianceReportId" as never, "=", existingDraft.id as string as never)
      .execute();

    // Delete existing report
    await db
      .deleteFrom("VarianceReport" as never)
      .where("id" as never, "=", existingDraft.id as string as never)
      .execute();
  }

  // Create variance report with status DRAFT
  const varianceReport = await db
    .insertInto("VarianceReport")
    .values({
      companyId: input.companyId,
      costObjectType: input.costObjectType,
      costObjectId: input.costObjectId,
      accountingPeriodId: input.accountingPeriodId,
      totalPlanned,
      totalActual,
      totalVariance,
      status: "DRAFT",
      createdAt: new Date(),
      updatedAt: null,
    })
    .returningAll()
    .executeTakeFirst();

  // Create variance report lines
  for (const line of lineData) {
    await db
      .insertInto("VarianceReportLine")
      .values({
        varianceReportId: (varianceReport as Record<string, unknown>).id as string,
        costElementId: line.costElementId,
        accountingPeriodId: input.accountingPeriodId,
        plannedAmount: line.plannedAmount,
        actualAmount: line.actualAmount,
        varianceAmount: line.varianceAmount,
        variancePercentage: line.variancePercentage,
        varianceDirection: line.varianceDirection,
        createdAt: new Date(),
        updatedAt: null,
      })
      .returningAll()
      .executeTakeFirst();
  }

  return ok({ varianceReport: varianceReport! });
}
