import type {
  PortfolioStatus,
  PortfolioGoal,
} from "./../../db/schema/portfolio";
import { formatNumber } from "@/app/utils";
import { and, eq, inArray } from "drizzle-orm";
import { db } from "@/app/db/db";
import { portfolio } from "@/app/db/schema/portfolio";
import { property } from "@/app/db/schema/property";
import type { Portfolio } from "@/app/db/schema/portfolio";
import type { PropertyWithRelations } from "@/app/db/schema/property";
import {
  plan,
  planRecommendations,
  recommendation,
  UnnestedRecommendation,
  PortfolioPlanRecommendation,
  scenario,
  ScenarioSelect,
} from "@/app/db/schema/recommendations";
import { sql } from "drizzle-orm";
import {
  FilterGroups,
  PropertyFilter,
  PROPERTY_TYPE_OPTIONS,
  BUILT_FORM_OPTIONS,
  TENURE_OPTIONS,
  YEAR_BUILT_OPTIONS,
  MAINFUEL_OPTIONS,
  EnumOption,
} from "@/app/utils/propertyFilters";
import { EPC_TO_SAP_MIN, EPC_TO_SAP_MAX } from "@/app/utils/epc";

const ENUM_FIELD_DB_OPTIONS: Record<string, EnumOption[]> = {
  propertyType: PROPERTY_TYPE_OPTIONS,
  builtForm:    BUILT_FORM_OPTIONS,
  tenure:       TENURE_OPTIONS,
  yearBuilt:    YEAR_BUILT_OPTIONS,
  mainfuel:     MAINFUEL_OPTIONS,
};

export interface PortfolioSettingsType {
  name: string;
  budget: number | null;
  goal: (typeof PortfolioGoal)[number];
  status: (typeof PortfolioStatus)[number];
}

export async function getPortfolioSettings(
  portfolioId: string
): Promise<PortfolioSettingsType> {
  //name, budget, goal, status
  const data = await db
    .select({
      name: portfolio.name,
      budget: portfolio.budget,
      goal: portfolio.goal,
      status: portfolio.status,
    })
    .from(portfolio)
    .where(eq(portfolio.id, BigInt(portfolioId)));

  if (data.length === 0) {
    throw new Error("Portfolio not found");
  }

  if (data.length > 1) {
    throw new Error("More than one portfolio found");
  }

  return data[0];
}

export async function getPortfolio(portfolioId: string): Promise<Portfolio> {
  const data = await db
    .select()
    .from(portfolio)
    .where(eq(portfolio.id, BigInt(portfolioId)));

  if (data.length === 0) {
    throw new Error("Portfolio not found");
  }

  if (data.length > 1) {
    throw new Error("More than one portfolio found");
  }

  return data[0];
}

export async function getPortfolioScenarios(
  portfolioId: string
): Promise<ScenarioSelect[]> {
  // This function will grab all scenarios from the database for a given portfolio
  const scenarios = await db
    .select()
    .from(scenario)
    .where(eq(scenario.portfolioId, BigInt(portfolioId)));

  return scenarios;
}

export async function getPortfolioPerformance(
  portfolioId: string
): Promise<ScenarioSelect[]> {
  // This function will grab portfolio metrics from the database, but it will firstly check for the existence of scenarios and
  // use those figures. If they don't exist, we will use the default figures from the portfolio table
  const scenarios = await db
    .select()
    .from(scenario)
    .where(eq(scenario.portfolioId, BigInt(portfolioId)));

  // If we have scenarios, return them
  if (scenarios.length > 0) {
    return scenarios;
  }

  return [];
}

// Types needed for the overview page
export interface ChartData {
  name: string;
  [key: string]: number | string; // Allows for any number of properties with numeric values, plus the name
}

interface Scenario {
  scenarioName: string; // E.g., "today", "baseline", or dynamically named scenarios
  data: ChartData[] | string; // This could be an array of chart data or a simple string if applicable
}

export interface DataItem {
  title: string;
  scenarios: Scenario[];
}

export async function getOverviewPortfolioData(
  portfolioId: string
): Promise<DataItem[]> {
  // Keep just the columns we need for the overview

  // We firstly attempt to retrieve the data from the scenarios table, starting with the default
  // scenario. If the default scenario doesn't exist, we'll use the data from the
  // portfolio
  let data = await db
    .select({
      name: scenario.name,
      cost: scenario.cost,
      funding: scenario.funding,
      contingency: scenario.contingency,
      epcBreakdownPreRetrofit: scenario.epcBreakdownPreRetrofit,
      epcBreakdownPostRetrofit: scenario.epcBreakdownPostRetrofit,
      numberOfProperties: scenario.numberOfProperties,
      nUnitsToRetrofit: scenario.nUnitsToRetrofit,
      co2PerUnitPreRetrofit: scenario.co2PerUnitPreRetrofit,
      co2PerUnitPostRetrofit: scenario.co2PerUnitPostRetrofit,
      energyBillPerUnitPreRetrofit: scenario.energyBillPerUnitPreRetrofit,
      energyBillPerUnitPostRetrofit: scenario.energyBillPerUnitPostRetrofit,
      energyConsumptionPerUnitPreRetrofit:
        scenario.energyConsumptionPerUnitPreRetrofit,
      energyConsumptionPerUnitPostRetrofit:
        scenario.energyConsumptionPerUnitPostRetrofit,
      valuationImprovementPerUnit: scenario.valuationImprovementPerUnit,
      costPerUnit: scenario.costPerUnit,
      costPerCo2Saved: scenario.costPerCo2Saved,
      costPerSapPoint: scenario.costPerSapPoint,
      valuationReturnOnInvestment: scenario.valuationReturnOnInvestment,
    })
    .from(scenario)
    .where(
      and(
        eq(scenario.portfolioId, BigInt(portfolioId)),
        eq(scenario.isDefault, true)
      )
    );

  if (data.length === 0) {
    // If we have no data, we fetch it from the scenarios. This will facilitate this for older portfolios
    const fallback = await db
      .select({
        name: portfolio.name,
        cost: portfolio.cost,
        epcBreakdownPreRetrofit: portfolio.epcBreakdownPreRetrofit,
        epcBreakdownPostRetrofit: portfolio.epcBreakdownPostRetrofit,
        numberOfProperties: portfolio.numberOfProperties,
        nUnitsToRetrofit: portfolio.nUnitsToRetrofit,
        co2PerUnitPreRetrofit: portfolio.co2PerUnitPreRetrofit,
        co2PerUnitPostRetrofit: portfolio.co2PerUnitPostRetrofit,
        energyBillPerUnitPreRetrofit: portfolio.energyBillPerUnitPreRetrofit,
        energyBillPerUnitPostRetrofit: portfolio.energyBillPerUnitPostRetrofit,
        energyConsumptionPerUnitPreRetrofit:
          portfolio.energyConsumptionPerUnitPreRetrofit,
        energyConsumptionPerUnitPostRetrofit:
          portfolio.energyConsumptionPerUnitPostRetrofit,
        valuationImprovementPerUnit: portfolio.valuationImprovementPerUnit,
        costPerUnit: portfolio.costPerUnit,
        costPerCo2Saved: portfolio.costPerCo2Saved,
        costPerSapPoint: portfolio.costPerSapPoint,
        valuationReturnOnInvestment: portfolio.valuationReturnOnInvestment,
      })
      .from(portfolio)
      .where(eq(portfolio.id, BigInt(portfolioId)));

    // funding and contingency are not available so we insert them
    data = fallback.map((x) => ({
      ...x,
      funding: 0,
      contingency: 0,
    }));
    if (data.length === 0) {
      throw new Error("Portfolio not found");
    }
  }

  if (data.length > 1) {
    throw new Error("More than one portfolio found");
  }

  const portfolioName = data[0].name;

  // Format the data we need for the overview
  const output: DataItem[] = [
    {
      title: "EPCs",
      scenarios: [
        {
          scenarioName: "Today",
          data: JSON.parse(
            data[0].epcBreakdownPreRetrofit || "[]"
          ) as ChartData[],
        },
        {
          scenarioName: portfolioName || "Default",
          data: JSON.parse(
            data[0].epcBreakdownPostRetrofit || "[]"
          ) as ChartData[],
        },
      ],
    },
    {
      title: "# Units",
      scenarios: [
        {
          scenarioName: "Today",
          data: String(data[0].numberOfProperties) || "",
        },
        {
          scenarioName: portfolioName || "Default",
          data: String(data[0].numberOfProperties) || "",
        },
      ],
    },
    {
      title: "# Units to retrofit",
      scenarios: [
        { scenarioName: "Today", data: "-" },
        {
          scenarioName: portfolioName || "Default",
          data: String(data[0].nUnitsToRetrofit) || "",
        },
      ],
    },
    {
      title: "CO2/unit",
      scenarios: [
        { scenarioName: "Today", data: data[0].co2PerUnitPreRetrofit || "" },
        {
          scenarioName: portfolioName || "Default",
          data: data[0].co2PerUnitPostRetrofit || "",
        },
      ],
    },
    {
      title: "Annual energy bill/unit",
      scenarios: [
        {
          scenarioName: "Today",
          data: data[0].energyBillPerUnitPreRetrofit || "",
        },
        {
          scenarioName: portfolioName || "Default",
          data: data[0].energyBillPerUnitPostRetrofit || "",
        },
      ],
    },
    {
      title: "Annual energy demand (kWh)/unit",
      scenarios: [
        {
          scenarioName: "Today",
          data: data[0].energyConsumptionPerUnitPreRetrofit || "",
        },
        {
          scenarioName: portfolioName || "Default",
          data: data[0].energyConsumptionPerUnitPostRetrofit || "",
        },
      ],
    },
    {
      title: "Cost of works (£)",
      scenarios: [
        { scenarioName: "Today", data: "" },
        {
          scenarioName: portfolioName || "Default",
          data: "£" + formatNumber(data[0].cost || 0),
        },
      ],
    },
    {
      title: "Cost of works (£)/unit",
      scenarios: [
        { scenarioName: "Today", data: "" },
        {
          scenarioName: portfolioName || "Default",
          data: data[0].costPerUnit || "",
        },
      ],
    },
    {
      title: "Funding (£)",
      scenarios: [
        { scenarioName: "Today", data: "" },
        {
          scenarioName: portfolioName || "Default",
          data: "£" + formatNumber(data[0].funding || 0),
        },
      ],
    },
    {
      title: "Funding (£)/unit",
      scenarios: [
        { scenarioName: "Today", data: "" },
        {
          scenarioName: portfolioName || "Default",
          data:
            "£" +
            formatNumber(
              (data[0].funding || 0) / (data[0].nUnitsToRetrofit || 1)
            ),
        },
      ],
    },
    {
      title: "Contingency (£)",
      scenarios: [
        { scenarioName: "Today", data: "" },
        {
          scenarioName: portfolioName || "Default",
          data: "£" + formatNumber(data[0].contingency || 0),
        },
      ],
    },
    {
      title: "Contingency (£)/unit",
      scenarios: [
        { scenarioName: "Today", data: "" },
        {
          scenarioName: portfolioName || "Default",
          data:
            "£" +
            formatNumber(
              (data[0].contingency || 0) / (data[0].nUnitsToRetrofit || 1)
            ),
        },
      ],
    },
    {
      title: "£ per CO2 reduction",
      scenarios: [
        { scenarioName: "Today", data: "" },
        {
          scenarioName: portfolioName || "Default",
          data: data[0].costPerCo2Saved || "",
        },
      ],
    },
    {
      title: "£ per SAP point",
      scenarios: [
        { scenarioName: "Today", data: "" },
        {
          scenarioName: portfolioName || "Default",
          data: data[0].costPerSapPoint || "",
        },
      ],
    },
    {
      title:
        "Potential valuation improvement/unit - highly indicative EPC effect*",
      scenarios: [
        { scenarioName: "Today", data: "" },
        {
          scenarioName: portfolioName || "Default",
          data: data[0].valuationImprovementPerUnit || "",
        },
      ],
    },
    {
      title:
        "Potential valuation return on investment - highly indicative EPC effect*",
      scenarios: [
        { scenarioName: "Today", data: "" },
        {
          scenarioName: portfolioName || "Default",
          data: data[0].valuationReturnOnInvestment || "",
        },
      ],
    },
  ];

  return output;
}

export async function getNonDefaultPortfolioScenarios(
  portfolioId: string
): Promise<{ id: bigint; name: string | null }[]> {
  // For a given portfolio Id, we want to retrieve all of the scenarios that are attributed to this portfolio
  // We exclude the default scenario
  const scenarios = await db
    .select({
      id: scenario.id,
      name: scenario.name,
    })
    .from(scenario)
    .where(
      and(
        eq(scenario.portfolioId, BigInt(portfolioId)),
        eq(scenario.isDefault, false)
      )
    );

  return scenarios;
}

type EpcLetter = "A" | "B" | "C" | "D" | "E" | "F" | "G";
const EPC_ORDER: EpcLetter[] = ["A", "B", "C", "D", "E", "F", "G"];

function buildEpcSapCondition(
  col: ReturnType<typeof sql>,
  operator: PropertyFilter["operator"],
  value: string
): ReturnType<typeof sql> | null {
  const letter = value as EpcLetter;

  if (operator === "epc_at_most") {
    const maxSap = EPC_TO_SAP_MAX[letter as keyof typeof EPC_TO_SAP_MAX];
    if (maxSap === undefined) return null;
    return sql`${col} <= ${maxSap}`;
  }

  if (operator === "epc_at_least") {
    const minSap = EPC_TO_SAP_MIN[letter as keyof typeof EPC_TO_SAP_MIN];
    if (minSap === undefined) return null;
    return sql`${col} >= ${minSap}`;
  }

  if (operator === "equals") {
    const minSap = EPC_TO_SAP_MIN[letter as keyof typeof EPC_TO_SAP_MIN];
    const maxSap = EPC_TO_SAP_MAX[letter as keyof typeof EPC_TO_SAP_MAX];
    if (minSap === undefined || maxSap === undefined) return null;
    return sql`${col} BETWEEN ${minSap} AND ${maxSap}`;
  }

  if (operator === "epc_less_than") {
    // Worse than the given letter — SAP below the band's minimum
    const minSap = EPC_TO_SAP_MIN[letter as keyof typeof EPC_TO_SAP_MIN];
    if (minSap === undefined) return null;
    return sql`${col} < ${minSap}`;
  }

  if (operator === "epc_greater_than") {
    // Better than the given letter — SAP above the band's maximum
    const maxSap = EPC_TO_SAP_MAX[letter as keyof typeof EPC_TO_SAP_MAX];
    if (maxSap === undefined) return null;
    return sql`${col} > ${maxSap}`;
  }

  if (operator === "epc_one_of") {
    const letters = value.split(",").map((l) => l.trim()) as EpcLetter[];
    const ranges = letters
      .map((l) => {
        const minSap = EPC_TO_SAP_MIN[l as keyof typeof EPC_TO_SAP_MIN];
        const maxSap = EPC_TO_SAP_MAX[l as keyof typeof EPC_TO_SAP_MAX];
        if (minSap === undefined || maxSap === undefined) return null;
        return sql`(${col} BETWEEN ${minSap} AND ${maxSap})`;
      })
      .filter((x): x is ReturnType<typeof sql> => x !== null);
    if (ranges.length === 0) return null;
    return sql`(${sql.join(ranges, sql` OR `)})`;
  }

  return null;
}

function buildConditionSql(filter: PropertyFilter): ReturnType<typeof sql> | null {
  switch (filter.field) {
    case "address":
      if (filter.operator === "contains") {
        return sql`p.address ILIKE ${"%" + filter.value + "%"}`;
      }
      return null;

    case "postcode":
      if (filter.operator === "starts_with") {
        return sql`p.postcode ILIKE ${filter.value + "%"}`;
      }
      return null;

    case "propertyRef":
      if (filter.operator === "contains") {
        return sql`p.landlord_property_id ILIKE ${"%" + filter.value + "%"}`;
      }
      return null;

    case "currentEpc":
      return buildEpcSapCondition(sql`p.current_sap_points`, filter.operator, filter.value);

    case "lodgedEpc":
      return buildEpcSapCondition(sql`p.original_sap_points`, filter.operator, filter.value);

    case "expectedEpc": {
      if (filter.operator === "epc_at_least") {
        const minSap = EPC_TO_SAP_MIN[filter.value as keyof typeof EPC_TO_SAP_MIN];
        if (minSap === undefined) return null;
        return sql`pl.post_sap_points IS NOT NULL AND pl.post_sap_points >= ${minSap}`;
      }
      return null;
    }

    case "epcExpiryDate": {
      const expiryExpr = sql`(epc.lodgement_date + INTERVAL '10 years')`;
      const guard = sql`epc.lodgement_date IS NOT NULL`;

      if (filter.operator === "date_before") {
        return sql`${guard} AND ${expiryExpr} < ${filter.value}::date`;
      }
      if (filter.operator === "date_after") {
        return sql`${guard} AND ${expiryExpr} > ${filter.value}::date`;
      }
      if (filter.operator === "date_equals") {
        return sql`${guard} AND DATE(${expiryExpr}) = ${filter.value}::date`;
      }
      if (filter.operator === "date_preset") {
        switch (filter.value) {
          case "expired":
            return sql`epc.is_expired = true`;
          case "expires_this_year":
            return sql`${guard} AND EXTRACT(YEAR FROM ${expiryExpr}) = EXTRACT(YEAR FROM CURRENT_DATE)`;
          case "expires_within_1_year":
            return sql`${guard} AND ${expiryExpr} BETWEEN CURRENT_DATE AND (CURRENT_DATE + INTERVAL '1 year')`;
          case "expires_within_2_years":
            return sql`${guard} AND ${expiryExpr} BETWEEN CURRENT_DATE AND (CURRENT_DATE + INTERVAL '2 years')`;
          default:
            return null;
        }
      }
      return null;
    }

    case "propertyType":
    case "builtForm":
    case "tenure":
    case "yearBuilt":
    case "mainfuel": {
      if (filter.operator !== "enum_one_of") return null;

      let selectedLabels: string[];
      try {
        selectedLabels = JSON.parse(filter.value);
      } catch {
        return null;
      }
      if (selectedLabels.length === 0) return null;

      const options = ENUM_FIELD_DB_OPTIONS[filter.field];
      const colMap: Record<string, ReturnType<typeof sql>> = {
        propertyType: sql`p.property_type`,
        builtForm:    sql`p.built_form`,
        tenure:       sql`p.tenure`,
        yearBuilt:    sql`p.year_built`,
        mainfuel:     sql`epc.mainfuel`,
      };
      const col = colMap[filter.field];

      // Flatten all dbValues for selected labels
      const allDbValues: string[] = [];
      let includeNull = false;

      for (const label of selectedLabels) {
        const opt = options.find((o) => o.label === label);
        if (!opt) continue;
        for (const v of opt.dbValues) {
          if (v === "__null__") {
            includeNull = true;
          } else {
            allDbValues.push(v);
          }
        }
      }

      const parts: ReturnType<typeof sql>[] = [];
      if (includeNull) {
        parts.push(sql`${col} IS NULL`);
      }
      if (allDbValues.length > 0) {
        // Build IN clause with each value as a separate param
        const placeholders = allDbValues.map((v) => sql`${v}`);
        parts.push(sql`${col} IN (${sql.join(placeholders, sql`, `)})`);
      }

      if (parts.length === 0) return null;
      if (parts.length === 1) return parts[0];
      return sql`(${sql.join(parts, sql` OR `)})`;
    }

    case "floorArea": {
      const n = parseFloat(filter.value);
      if (isNaN(n)) return null;
      if (filter.operator === "num_gte") return sql`epc.total_floor_area >= ${n}`;
      if (filter.operator === "num_lte") return sql`epc.total_floor_area <= ${n}`;
      if (filter.operator === "num_equals") return sql`epc.total_floor_area = ${n}`;
      return null;
    }

    case "co2Emissions": {
      const n = parseFloat(filter.value);
      if (isNaN(n)) return null;
      if (filter.operator === "num_gte") return sql`epc.co2_emissions >= ${n}`;
      if (filter.operator === "num_lte") return sql`epc.co2_emissions <= ${n}`;
      if (filter.operator === "num_equals") return sql`epc.co2_emissions = ${n}`;
      return null;
    }
  }
  return null;
}

function buildWhereClause(filterGroups: FilterGroups): ReturnType<typeof sql> {
  const groupFragments: ReturnType<typeof sql>[] = [];

  for (const group of filterGroups) {
    const condFragments = group.conditions
      .map(buildConditionSql)
      .filter((x): x is ReturnType<typeof sql> => x !== null);

    if (condFragments.length === 0) continue;

    if (condFragments.length === 1) {
      groupFragments.push(condFragments[0]);
    } else {
      groupFragments.push(sql`(${sql.join(condFragments, sql` AND `)})`);
    }
  }

  return groupFragments.length > 0
    ? sql`AND (${sql.join(groupFragments, sql` OR `)})`
    : sql``;
}

export async function getPropertiesCount(
  portfolioId: string,
  filterGroups: FilterGroups = []
): Promise<number> {
  const combinedWhere = buildWhereClause(filterGroups);

  const result = await db.execute<{ count: string }>(sql`
    SELECT COUNT(DISTINCT p.id)::int AS count
    FROM property p
    LEFT JOIN property_details_epc epc ON epc.property_id = p.id
    LEFT JOIN plan pl ON pl.property_id = p.id AND pl.is_default = true
    WHERE p.portfolio_id = ${portfolioId}
    ${combinedWhere}
  `);

  return parseInt(result.rows[0]?.count ?? "0", 10);
}

export async function getProperties(
  portfolioId: string,
  limit: number = 1000,
  offset: number = 0,
  filterGroups: FilterGroups = []
): Promise<PropertyWithRelations[]> {
  // We need to perform the query like this because the nested query is not supported in the ORM right now

  const combinedWhere = buildWhereClause(filterGroups);

  const result =
    await db.execute<PropertyWithRelations>(sql<PropertyWithRelations>`
    SELECT
      p.id AS id,
      p.portfolio_id AS "portfolioId",
      p.address AS address,
      p.postcode AS postcode,
      p.status AS status,
      p.creation_status AS "creationStatus",
      p.current_epc_rating AS "currentEpcRating",
      p.current_sap_points AS "currentSapPoints",
      t.epc AS "targetEpc",
      pl.id AS "planId",
      fp.scheme AS "fundingScheme",
      COALESCE(SUM(r.sap_points), 0) AS "totalRecommendationSapPoints",
      COALESCE(SUM(r.estimated_cost), 0) AS "totalRecommendationCost",
      p.landlord_property_id AS "landlordPropertyId",
      p.original_sap_points AS "originalSapPoints",
      p.property_type AS "propertyType",
      p.built_form AS "builtForm",
      p.tenure AS tenure,
      p.year_built AS "yearBuilt",
      epc.lodgement_date::text AS "epcLodgementDate",
      epc.is_expired AS "epcIsExpired",
      epc.total_floor_area AS "totalFloorArea",
      epc.co2_emissions AS "co2Emissions",
      epc.mainfuel AS mainfuel
    FROM property p
    LEFT JOIN property_targets t
      ON t.property_id = p.id
    LEFT JOIN plan pl
      ON pl.property_id = p.id
    AND pl.is_default = true
    LEFT JOIN funding_package fp
      ON fp.plan_id = pl.id
    LEFT JOIN plan_recommendations pr
      ON pr.plan_id = pl.id
    LEFT JOIN recommendation r
      ON r.id = pr.recommendation_id
    AND r.default = true
    AND r.already_installed = false
    LEFT JOIN property_details_epc epc
      ON epc.property_id = p.id
    WHERE p.portfolio_id = ${portfolioId}
    ${combinedWhere}
    GROUP BY
      p.id,
      p.portfolio_id,
      p.address,
      p.postcode,
      p.status,
      p.creation_status,
      p.current_epc_rating,
      p.current_sap_points,
      t.epc,
      pl.id,
      fp.scheme,
      p.landlord_property_id,
      p.original_sap_points,
      p.property_type,
      p.built_form,
      p.tenure,
      p.year_built,
      epc.lodgement_date,
      epc.is_expired,
      epc.total_floor_area,
      epc.co2_emissions,
      epc.mainfuel
    LIMIT ${limit} OFFSET ${offset};
  `);

  const data: PropertyWithRelations[] = result.rows;

  return data;
}

interface UnaggregatedPortfolioPlanRecommendation {
  quantity: number | null; // Allow null for cases where no materials are associated
  quantityUnit: string | null;
  estimatedCost: number;
  materialType: string | null;
  propertyId?: string;
  numberOfProperties?: number; // Optional field to hold the count of unique propertyIds
  measureType: string | null;
  measureTypeCount: number | undefined; // Optional field to hold the count of unique measureTypes
}

function aggregateRecommendations(
  data: UnaggregatedPortfolioPlanRecommendation[]
): PortfolioPlanRecommendation[] {
  const grouped: {
    [key: string]: PortfolioPlanRecommendation & {
      propertyIds: Set<string>;
      measureTypes: Set<string>;
    };
  } = {};

  data.forEach((item) => {
    // Use the combination of quantityUnit and materialType (or measureType if materialType is missing) as a unique key
    const key = item.materialType
      ? `${item.quantityUnit}_${item.materialType}`
      : `${item.quantityUnit}_${item.measureType}`;

    if (!grouped[key]) {
      grouped[key] = {
        ...item,
        numberOfProperties: 0, // Initialize to 0
        propertyIds: item.propertyId ? new Set([item.propertyId]) : new Set(),
        measureTypes: item.measureType
          ? new Set([item.measureType])
          : new Set(),
      };
    } else {
      // if quantity is null previously, set to 0
      grouped[key].quantity =
        (grouped[key].quantity ?? 0) + (item.quantity ?? 0);
      grouped[key].estimatedCost += item.estimatedCost;

      if (item.propertyId) {
        grouped[key].propertyIds.add(item.propertyId);
      }

      if (item.measureType) {
        grouped[key].measureTypes.add(item.measureType);
      }
    }
  });

  // Round the results to 2 decimal places, compute uniquePropertyCount, and count unique measureTypes
  for (const key in grouped) {
    grouped[key].quantity = parseFloat(
      grouped[key].quantity?.toFixed(2) || "0.00"
    );
    grouped[key].estimatedCost = parseFloat(
      grouped[key].estimatedCost.toFixed(2)
    );
    grouped[key].numberOfProperties = grouped[key].propertyIds.size;
    grouped[key].measureTypeCount = grouped[key].measureTypes.size;

    // Cleanup temporary properties
    delete (grouped[key] as any).propertyIds; // using type assertion to bypass the TS error
    delete (grouped[key] as any).measureTypes; // using type assertion to bypass the TS error
  }

  return Object.values(grouped);
}

export async function getPortfolioMeasures(portfolioId: string) {
  // Step 1: Get all default plans for the portfolioId
  const plans = await db.query.plan.findMany({
    where: and(
      eq(property.portfolioId, BigInt(portfolioId)),
      eq(plan.isDefault, true)
    ),
  });

  const planIds = plans.map((plan) => plan.id);

  // Step 2: Get recommendations for the plans
  const recommendations = await db.query.planRecommendations.findMany({
    where: inArray(planRecommendations.planId, planIds),
  });

  const recommendationIds = recommendations.map(
    (recommendation) => recommendation.id
  );

  // Step 3: Get data for recommendations, including materials and measureTypes
  const data = await db.query.recommendation.findMany({
    where: and(
      inArray(recommendation.id, recommendationIds),
      eq(recommendation.default, true)
    ),
    columns: { propertyId: true, measureType: true },
    with: {
      recommendationMaterials: {
        with: {
          material: {
            columns: {
              type: true,
            },
          },
        },
        columns: {
          quantity: true,
          quantityUnit: true,
          estimatedCost: true,
        },
      },
    },
  });

  // Step 4: Handle recommendations with or without materials
  const unnestedRecommendations: UnnestedRecommendation[] = data.reduce(
    (acc: UnnestedRecommendation[], recommendation) => {
      if (recommendation.recommendationMaterials.length === 0) {
        // Handle case where no materials are associated
        acc.push({
          quantity: 0,
          quantityUnit: null,
          estimatedCost: 0,
          materialType: null,
          propertyId: String(recommendation.propertyId),
          measureType: recommendation.measureType, // Ensure measureType is included
          measureTypeCount: undefined, // Initialize to undefined
        });
      } else {
        // Process recommendations with materials
        const materials = recommendation.recommendationMaterials.map(
          (material) => ({
            quantity: material.quantity,
            quantityUnit: material.quantityUnit,
            estimatedCost: material.estimatedCost,
            materialType: material.material.type,
            propertyId: String(recommendation.propertyId),
            measureType: recommendation.measureType, // Include measureType
            measureTypeCount: undefined, // Initialize to undefined
          })
        );
        acc.push(...materials);
      }
      return acc;
    },
    []
  );

  // Step 5: Aggregate the recommendations
  const aggregated = aggregateRecommendations(unnestedRecommendations);

  return aggregated;
}
