export type FilterField =
  | "address"
  | "postcode"
  | "currentEpc"
  | "expectedEpc"
  | "lodgedEpc"
  | "epcExpiryDate"
  | "propertyRef"
  | "propertyType"
  | "builtForm"
  | "yearBuilt"
  | "provenance"
  | "floorArea"
  | "co2Emissions"
  | "mainfuel"
  | "wallType"
  | "roofType"
  | "heatingSystem"
  | "tags";

export type FilterOperator =
  | "contains"
  | "starts_with"
  | "equals"
  | "epc_at_least"
  | "epc_at_most"
  | "epc_less_than"
  | "epc_greater_than"
  | "epc_one_of"
  | "date_before"
  | "date_after"
  | "date_equals"
  | "date_preset"
  | "enum_one_of"
  | "num_gte"
  | "num_lte"
  | "num_equals";

export type DatePreset =
  | "expired"
  | "expires_this_year"
  | "expires_within_1_year"
  | "expires_within_2_years";

export interface PropertyFilter {
  id: string;
  field: FilterField;
  operator: FilterOperator;
  value: string;
}

export interface FilterGroup {
  id: string;
  conditions: PropertyFilter[];
}

export type FilterGroups = FilterGroup[];

/* -----------------------------------------------------------------------
   Enum option definitions for categorical filter fields
------------------------------------------------------------------------ */

export interface EnumOption {
  /** User-facing display label */
  label: string;
  /** Actual DB values to match. Use ["__null__"] to match NULL. */
  dbValues: string[];
}

// dbValues are the label vocabulary emitted by resolvedPropertyTypeSql (override
// snapshot / EPC-derived label). "Park home" is reachable from an RdSAP code (4)
// or an override; "Unknown" is the never-null terminal + an explicit override —
// it selects properties with no resolvable value at all (ADR-0012).
export const PROPERTY_TYPE_OPTIONS: EnumOption[] = [
  { label: "House",       dbValues: ["House"] },
  { label: "Flat",        dbValues: ["Flat"] },
  { label: "Bungalow",    dbValues: ["Bungalow"] },
  { label: "Maisonette",  dbValues: ["Maisonette"] },
  { label: "Park home",   dbValues: ["Park home"] },
  { label: "Unknown",     dbValues: ["Unknown"] },
];

// dbValues are the label vocabulary emitted by resolvedBuiltFormSql (override
// snapshot / EPC-derived label). "Not Recorded" matches only an explicit
// override of that literal; "Unknown" is the never-null terminal + an explicit
// override — it selects properties with no resolvable value at all (ADR-0012).
export const BUILT_FORM_OPTIONS: EnumOption[] = [
  { label: "Detached",              dbValues: ["Detached"] },
  { label: "Semi-Detached",         dbValues: ["Semi-Detached"] },
  { label: "End-Terrace",           dbValues: ["End-Terrace"] },
  { label: "Mid-Terrace",           dbValues: ["Mid-Terrace"] },
  { label: "Enclosed End-Terrace",  dbValues: ["Enclosed End-Terrace"] },
  { label: "Enclosed Mid-Terrace",  dbValues: ["Enclosed Mid-Terrace"] },
  { label: "Not Recorded",          dbValues: ["Not Recorded"] },
  { label: "Unknown",               dbValues: ["Unknown"] },
];

// Provenance signal (ADR-0002). dbValues are the raw signal strings emitted by
// provenanceSignalSql / resolveProvenanceSignal.
export const PROVENANCE_OPTIONS: EnumOption[] = [
  { label: "Predicted",   dbValues: ["estimated"] },
  { label: "Re-modelled", dbValues: ["remodelled"] },
  { label: "Standard",    dbValues: ["none"] },
];

// Construction Age is band-native (ADR-0012): labels + dbValues are the RdSAP
// band labels emitted by resolvedConstructionAgeSql. These MUST match
// CONSTRUCTION_AGE_BANDS[*].label in epcSources.ts exactly (note the en-dash);
// epcSources.test.ts guards against drift. "Unknown" is the never-null terminal.
export const YEAR_BUILT_OPTIONS: EnumOption[] = [
  "before 1900", "1900–1929", "1930–1949", "1950–1966", "1967–1975",
  "1976–1982", "1983–1990", "1991–1995", "1996–2002", "2003–2006",
  "2007–2011", "2012–2022", "2023 onwards", "Unknown",
].map((label) => ({ label, dbValues: [label] }));

// RdSAP `main_fuel` code → coarse label. The new EPC graph stores the main fuel
// as this numeric code on epc_main_heating_detail.main_fuel_type (see mainfuelSql
// / resolveMainFuel), so every code that can appear must fold into a filter
// option or an overridden/EPC-derived fuel would display but be unfilterable
// (ADR-0012). Codes are the authoritative Model `epc_codes.csv` `main_fuel` table
// (RdSAP), cross-checked against production epc rows: 26→mains gas, 29/39→electric,
// 0/20/25/31/57→"Community scheme", 5/15/33→coal, 6→wood logs, 9→dual fuel. All
// "(community)"/heat-network codes bucket to "Community Heat Network" — being on a
// communal scheme is the salient fact; the scheme's own fuel is a secondary detail.
export const RDSAP_MAIN_FUEL_CODE_LABELS: Record<string, string> = {
  // Mains gas
  "1": "Mains Gas", "26": "Mains Gas",
  // LPG
  "2": "LPG", "3": "LPG", "17": "LPG", "27": "LPG", "38": "LPG",
  // Oil
  "4": "Oil", "28": "Oil",
  // Electricity
  "10": "Electricity", "29": "Electricity", "39": "Electricity",
  // Coal / mineral solid fuel (anthracite, smokeless, house coal)
  "5": "Coal", "14": "Coal", "15": "Coal", "33": "Coal",
  // Biomass / wood (logs, pellets, chips)
  "6": "Biomass", "7": "Biomass", "8": "Biomass", "12": "Biomass", "16": "Biomass",
  // Dual fuel
  "9": "Dual Fuel (Mineral + Wood)",
  // Biogas
  "13": "Biogas", "51": "Biogas",
  // Bioethanol
  "19": "Bioethanol", "76": "Bioethanol",
  // Biodiesel (from biomass / used cooking oil / vegetable / rape seed oil)
  "34": "Biodiesel", "35": "Biodiesel", "36": "Biodiesel", "37": "Biodiesel",
  "71": "Biodiesel", "72": "Biodiesel", "73": "Biodiesel", "74": "Biodiesel",
  // Biodiesel blend
  "18": "B30k (Biodiesel blend)", "75": "B30k (Biodiesel blend)",
  // Waste combustion
  "11": "Waste Combustion",
  // Community / heat-network (the dwelling is on a communal scheme)
  "0": "Community Heat Network", "20": "Community Heat Network", "21": "Community Heat Network",
  "22": "Community Heat Network", "23": "Community Heat Network", "24": "Community Heat Network",
  "25": "Community Heat Network", "30": "Community Heat Network", "31": "Community Heat Network",
  "32": "Community Heat Network", "41": "Community Heat Network", "42": "Community Heat Network",
  "43": "Community Heat Network", "44": "Community Heat Network", "45": "Community Heat Network",
  "46": "Community Heat Network", "47": "Community Heat Network", "48": "Community Heat Network",
  "49": "Community Heat Network", "50": "Community Heat Network", "52": "Community Heat Network",
  "53": "Community Heat Network", "54": "Community Heat Network", "55": "Community Heat Network",
  "56": "Community Heat Network", "57": "Community Heat Network", "58": "Community Heat Network",
  "99": "Community Heat Network",
};

// EPR free-text main_fuel_type strings (Elmhurst/PASHub, ~5% of new-approach
// rows) folded to the same labels. The empty-string sentinel is NULLIF'd to NULL
// upstream and resolves to 'Unknown'.
export const EPR_MAIN_FUEL_STRING_LABELS: Record<string, string> = {
  "Mains gas": "Mains Gas",
  "Electricity": "Electricity",
  "Bulk LPG": "LPG",
  "Heating oil": "Oil",
};

// dbValues fold several vocabularies onto one user-facing label: the raw legacy
// EPC mainfuel strings (property_details_epc), the landlord main_fuel override
// values ("mains gas", "LPG (bulk)", … — see MainFuelValues), the new-approach
// RdSAP numeric codes (RDSAP_MAIN_FUEL_CODE_LABELS) and the EPR strings
// (EPR_MAIN_FUEL_STRING_LABELS). resolvedMainFuelSql emits whichever applies; all
// must resolve to the same chip (ADR-0012). descriptorResolution.test asserts
// every override value, RdSAP code and EPR string is covered here.
const MAINFUEL_OPTIONS_BASE: EnumOption[] = [
  { label: "Mains Gas",                    dbValues: ["Gas mains gas", "Mains gas community", "Mains gas not community", "Mains gas this is for backwards compatibility only and should not be used", "mains gas", "mains gas (community)"] },
  { label: "LPG",                          dbValues: ["Bottled lpg", "Lpg community", "Lpg not community", "Lpg this is for backwards compatibility only and should not be used", "LPG (bulk)", "bottled LPG", "LPG special condition"] },
  { label: "Oil",                          dbValues: ["Oil heating oil", "Oil community", "Oil not community", "Oil this is for backwards compatibility only and should not be used", "oil"] },
  { label: "Electricity",                  dbValues: ["Electricity electricity unspecified tariff", "Electricity community", "Electricity not community", "Electricity this is for backwards compatibility only and should not be used", "electricity", "electricity (community)"] },
  { label: "Biomass",                      dbValues: ["Bulk wood pellets", "Wood chips", "Wood logs", "Biomass community", "Biomass this is for backwards compatibility only and should not be used", "biomass (community)", "wood logs"] },
  { label: "Coal",                         dbValues: ["Anthracite", "House coal not community", "House coal this is for backwards compatibility only and should not be used", "Smokeless coal", "Coal community", "house coal", "smokeless coal"] },
  { label: "Dual Fuel (Mineral + Wood)",   dbValues: ["Dual fuel mineral wood", "dual fuel (mineral and wood)"] },
  { label: "Biogas",                       dbValues: ["Biogas not community"] },
  { label: "Bioethanol",                   dbValues: [] },
  { label: "Biodiesel",                    dbValues: ["Heat from boilers using biodiesel from any biomass source community"] },
  { label: "B30d (Biodiesel blend)",       dbValues: ["B30d community"] },
  { label: "B30k (Biodiesel blend)",       dbValues: ["B30k not community"] },
  { label: "Waste Combustion",             dbValues: [] },
  { label: "Community Heat Network",       dbValues: ["From heat network data community"] },
  { label: "No Heating System",            dbValues: ["To be used only when there is no heatinghotwater system", "To be used only when there is no heatinghotwater system or data is from a community network"] },
  { label: "Unknown / No Data",            dbValues: ["UNKNOWN", "NO DATA!", "Unknown"] },
];

/** Fold the RdSAP code + EPR string vocabularies into each label's dbValues. */
export const MAINFUEL_OPTIONS: EnumOption[] = MAINFUEL_OPTIONS_BASE.map((opt) => {
  const codes = Object.entries(RDSAP_MAIN_FUEL_CODE_LABELS)
    .filter(([, label]) => label === opt.label)
    .map(([code]) => code);
  const strings = Object.entries(EPR_MAIN_FUEL_STRING_LABELS)
    .filter(([, label]) => label === opt.label)
    .map(([str]) => str);
  return { ...opt, dbValues: [...opt.dbValues, ...codes, ...strings] };
});

/* -----------------------------------------------------------------------
   Free-text descriptor buckets: Wall Type / Roof Type / Heating System (ADR-0012)

   Unlike the enum fields above (exact dbValues), the resolved wall/roof/heating
   value is a free-text description (override vocabulary, EPC
   epc_energy_element.description, or legacy property_details_epc column) with
   HIGH cardinality — 88+ distinct EPC wall descriptions, 245 legacy, plus
   insulation-thickness variants, "Average thermal transmittance …" U-values and
   encoding corruption. Exact matching can't scale, so each coarse bucket is a set
   of case-insensitive NEEDLES matched by prefix (wall/roof) or substring
   (heating, whose fuel follows a comma). classifyDescriptor is the pure twin of
   the SQL bucketing in the portfolio query's buildConditionSql — keep in sync.
   Ordering matters: the first bucket whose needle matches wins.
------------------------------------------------------------------------ */

export interface DescriptorBucket {
  label: string;
  /** Lowercase needles, matched per the field's DescriptorMatch mode. Empty = the Unknown terminal (matches the complement). */
  needles: string[];
}

export type DescriptorMatch = "prefix" | "contains";

// Wall construction. Prefix-matched on the leading construction term. "Stone"
// folds sandstone/limestone + granite/whinstone; "Average thermal transmittance …"
// (a U-value, not a construction type) and corruption fall through to Unknown.
export const WALL_TYPE_OPTIONS: DescriptorBucket[] = [
  { label: "Cavity",        needles: ["cavity"] },
  { label: "Solid Brick",   needles: ["solid brick"] },
  { label: "Timber Frame",  needles: ["timber frame"] },
  { label: "Stone",         needles: ["sandstone", "granite"] },
  { label: "System Built",  needles: ["system built"] },
  { label: "Park Home",     needles: ["park home"] },
  { label: "Cob",           needles: ["cob"] },
  { label: "Curtain Wall",  needles: ["curtain"] },
  { label: "Basement",      needles: ["basement"] },
  { label: "Unknown",       needles: [] },
];

// Roof form. Prefix-matched. "Premises Above" folds the "(another dwelling
// above)" / "(other premises above)" family (a flat with a dwelling above it, so
// no own roof). U-values fall through to Unknown.
export const ROOF_TYPE_OPTIONS: DescriptorBucket[] = [
  { label: "Pitched",        needles: ["pitched"] },
  { label: "Flat",           needles: ["flat"] },
  { label: "Room-in-Roof",   needles: ["roof room"] },
  { label: "Thatched",       needles: ["thatched"] },
  { label: "Premises Above", needles: ["(another", "(other", "(same", "another dwelling", "another premises", "other premises"] },
  { label: "Unknown",        needles: [] },
];

// Heating system. Substring-matched (the fuel follows a comma, e.g. "Boiler and
// radiators, mains gas"). Order is load-bearing: Heat Pump / Community / Solid
// Fuel / Oil / LPG are checked before the gas + electric buckets so a
// heat-pump/community/wood/oil/LPG system isn't miscaught by "gas"/"electric";
// Gas Room Heater before Gas Boiler; Electric Storage before Direct Electric.
export const HEATING_SYSTEM_OPTIONS: DescriptorBucket[] = [
  { label: "Heat Pump",        needles: ["heat pump"] },
  { label: "Community",        needles: ["community"] },
  { label: "Solid Fuel",       needles: ["coal", "anthracite", "smokeless", "wood", "dual fuel", "biomass", "solid fuel"] },
  // NB: bare "oil" is a substring of "b(oil)er" — match the fuel token only (", oil"
  // as in "Boiler and radiators, oil") and the leading override "Oil room heater".
  { label: "Oil",              needles: [", oil", "oil room heater"] },
  { label: "LPG",              needles: ["lpg"] },
  { label: "Gas Room Heater",  needles: ["gas room heater", "room heaters, mains gas", "room heaters, gas"] },
  { label: "Gas Boiler",       needles: ["mains gas", "gas boiler", "gas cpsu"] },
  { label: "Electric Storage", needles: ["storage heater"] },
  { label: "Direct Electric",  needles: ["electric"] },
  { label: "Unknown",          needles: [] },
];

/** Filter config per descriptor field: its buckets + how needles are matched. */
export const DESCRIPTOR_FILTER_CONFIG: Record<
  "wallType" | "roofType" | "heatingSystem",
  { buckets: DescriptorBucket[]; match: DescriptorMatch }
> = {
  wallType:      { buckets: WALL_TYPE_OPTIONS, match: "prefix" },
  roofType:      { buckets: ROOF_TYPE_OPTIONS, match: "prefix" },
  heatingSystem: { buckets: HEATING_SYSTEM_OPTIONS, match: "contains" },
};

/**
 * Classify a resolved free-text descriptor into its coarse filter bucket: the
 * first bucket with a matching needle, else "Unknown". Pure twin of the SQL
 * bucketing in buildConditionSql (portfolio query) — keep in sync.
 */
export function classifyDescriptor(
  value: string | null | undefined,
  buckets: DescriptorBucket[],
  match: DescriptorMatch,
): string {
  if (value == null) return "Unknown";
  const v = value.toLowerCase();
  for (const bucket of buckets) {
    for (const needle of bucket.needles) {
      if (match === "prefix" ? v.startsWith(needle) : v.includes(needle)) {
        return bucket.label;
      }
    }
  }
  return "Unknown";
}
