/**
 * EPC read-model resolver.
 *
 * The legacy plan engine wrote a flat per-property table `property_details_epc`.
 * The new modelling pipeline (Hestia-Homes/Model) does NOT write that table —
 * it writes the normalized EPC graph (`epc_property` + children), the modelling
 * output (`plan` / `recommendation`) and `property_baseline_performance`.
 *
 * A property is "new approach" when `property.updated_at >= 2026-06-01` (the
 * cutoff — the old pipeline predates it). For such properties the app must read
 * the new sources and must NOT touch `property_details_epc`. Legacy properties
 * keep reading the old table unchanged.
 *
 * This module is the single place that knows the cutoff and the field→source
 * mapping, so callers (building-passport, property-meta, portfolio list,
 * reporting + scenario metrics) branch consistently.
 *
 * Mapping applied (see the handover report for the full table and flagged gaps):
 *   - EPC fabric descriptions + 1–5 ratings → `epc_energy_element`
 *   - whole-dwelling counts / flags        → `epc_property` (+ `epc_flat_details`)
 *   - baseline carbon / energy / bills     → `property_baseline_performance`
 *   - post-retrofit data                   → `plan` / `recommendation`
 *   - "estimated" flag                     → property has only a `source='predicted'`
 *                                            EPC (no `source='lodged'` row)
 *
 * Gracefully-omitted gaps for new properties (no clean equivalent — reported to
 * the owner): `mainfuel`, `energyTariff`, `floorHeight`, the installed-measures
 * bill adjustment, and the SAP-05 "likely downgrade" metric.
 */
import { db } from "@/app/db/db";
import { and, eq, sql } from "drizzle-orm";
import { resolveProvenanceSignal, type ProvenanceSignal } from "./provenance";
import { PROPERTY_TYPE_LABELS, BUILT_FORM_LABELS } from "@/lib/epc/labels";
import {
  epcProperty,
  epcEnergyElement,
  epcFlatDetails,
  epcBuildingPart,
  epcFloorDimension,
  propertyBaselinePerformance,
  propertyDetailsEpc,
  property,
} from "@/app/db/schema/property";

/** The discriminator: properties updated on/after this date use the new sources. */
export const NEW_APPROACH_CUTOFF = "2026-06-01";

export function isNewApproach(updatedAt: Date | string | null | undefined): boolean {
  if (!updatedAt) return false;
  const d = updatedAt instanceof Date ? updatedAt : new Date(updatedAt);
  return d >= new Date(`${NEW_APPROACH_CUTOFF}T00:00:00.000Z`);
}

// ─────────────────────────────────────────────────────────────────────────────
// Shared SQL fragments for set-based aggregate queries.
//
// These assume the standard aliases:
//   p   = property
//   bp  = property_baseline_performance   (new — baseline carbon/energy/bills)
//   epl = epc_property WHERE source='lodged'
//   epp = epc_property WHERE source='predicted'
// and take the legacy `property_details_epc` alias as a fragment so each query
// can keep its own (`e` vs `epc`).
//
// Add `newApproachJoins` to any query that uses one of the *Sql expressions.
// ─────────────────────────────────────────────────────────────────────────────

type Alias = ReturnType<typeof sql>;

/** TRUE for new-approach rows. */
export const isNewApproachSql = sql`p.updated_at >= ${NEW_APPROACH_CUTOFF}::date`;

/**
 * LEFT JOINs that expose the new sources alongside the legacy table.
 *
 * `epc_property.source` has three values, not two. `lodged` is a real certificate.
 * The other two share one slot — the *predicted slot* — and are both gap-fills:
 *
 *   - `predicted` — synthesised blind from the postcode cohort. Nothing is known
 *     about this dwelling's own energy performance; there is no certificate for it
 *     anywhere.
 *   - `expired` — synthesised, but conditioned on the dwelling's own expired
 *     pre-2012 certificate from the historic register (Model ADR-0054). The gov EPC
 *     API only serves certificates registered since 2012, so these dwellings look
 *     EPC-less to ingestion even though a real, stale certificate exists.
 *
 * That distinction is the whole point: a home with an `expired` row **has** an EPC
 * (an expired one); a home with only a `predicted` row has none at all.
 *
 * Matching `source = 'predicted'` alone would silently drop every `expired` row —
 * it would match neither `epl` nor `epp`, and the home would read as though it had
 * a valid current certificate. Address the *slot* (`epp` OR `epx`), never one member.
 * Mirrors PREDICTED_SLOT_SOURCES in the Model repo.
 */
export const newApproachJoins = sql`
  LEFT JOIN property_baseline_performance bp ON bp.property_id = p.id
  LEFT JOIN epc_property epl ON epl.property_id = p.id AND epl.source = 'lodged'
  LEFT JOIN epc_property epp ON epp.property_id = p.id AND epp.source = 'predicted'
  LEFT JOIN epc_property epx ON epx.property_id = p.id AND epx.source = 'expired'
`;

/**
 * Baseline carbon (t CO₂/yr). New: the effective (re-baselined) value — the
 * canonical "current" per ADR-0002, and the baseline the plans' post_* figures
 * were scored against. The lodged value is NULL for predicted-only properties
 * (no mirrored estimate is written), which made lodged-based reporting totals
 * silently exclude them — and drove "carbon saved" negative whenever their
 * plans' post carbon counted on the scenario side. Legacy: e.co2_emissions.
 */
export const carbonSql = (e: Alias) =>
  sql`CASE WHEN ${isNewApproachSql} THEN bp.effective_co2_emissions_t_per_yr ELSE ${e}.co2_emissions END`;

/** Primary energy intensity (kWh/m²/yr). New: effective value (ADR-0002, as carbonSql). Legacy: e.primary_energy_consumption. */
export const energyConsumptionSql = (e: Alias) =>
  sql`CASE WHEN ${isNewApproachSql} THEN bp.effective_primary_energy_intensity_kwh_per_m2_yr ELSE ${e}.primary_energy_consumption END`;

/**
 * Baseline SAP score. New: the lodged (gov-registry) score from
 * property_baseline_performance (the property-row current_sap_points isn't
 * written for new-approach properties). Legacy: property.current_sap_points.
 * Swap to `effective_sap_score` for the modelling-adjusted baseline.
 */
export const sapSql = sql`CASE WHEN ${isNewApproachSql} THEN bp.lodged_sap_score ELSE p.current_sap_points END`;

/** Baseline EPC band. New: lodged band from property_baseline_performance. Legacy: property.current_epc_rating. */
export const epcBandSql = sql`CASE WHEN ${isNewApproachSql} THEN bp.lodged_epc_band ELSE p.current_epc_rating END`;

/**
 * Effective (re-baselined) SAP score / EPC band — the canonical "current" for
 * display and reporting (ADR-0002). New: bp.effective_*; legacy: the property
 * row. These replaced the lodged `sapSql`/`epcBandSql` above as the table +
 * reporting source; those lodged fragments are intentionally retained (unused
 * for now) for a possible future register-fidelity reporting view (ADR-0002).
 * The "Lodged EPC" column itself uses the source-gated `lodgedEpcBandSql` below.
 */
export const effectiveSapSql = sql`CASE WHEN ${isNewApproachSql} THEN bp.effective_sap_score ELSE p.current_sap_points END`;
export const effectiveEpcBandSql = sql`CASE WHEN ${isNewApproachSql} THEN bp.effective_epc_band ELSE p.current_epc_rating END`;

/**
 * Lodged EPC band for the "Lodged EPC" column — only when a real certificate
 * exists (`source = lodged`, i.e. the lodged epc_property row `epl`). A
 * predicted property carries lodged_* (mirrored estimates) that must not
 * surface, so it renders NULL. Legacy: derive from the property row's
 * original_sap_points (handled in the column, kept as the row value here).
 */
export const lodgedEpcBandSql = sql`CASE WHEN ${isNewApproachSql} THEN (CASE WHEN epl.id IS NOT NULL THEN bp.lodged_epc_band ELSE NULL END) ELSE p.current_epc_rating END`;

/** Lodged SAP score for the "Lodged EPC" filter — same source/gate as lodgedEpcBandSql. Legacy: the row's original_sap_points. */
export const lodgedSapSql = sql`CASE WHEN ${isNewApproachSql} THEN (CASE WHEN epl.id IS NOT NULL THEN bp.lodged_sap_score ELSE NULL END) ELSE p.original_sap_points END`;

/**
 * Provenance signal for the portfolio table (mirrors resolveProvenanceSignal /
 * provenance.ts). New-approach only; legacy → 'none'.
 */
export const provenanceSignalSql = sql`CASE
  WHEN ${isNewApproachSql} THEN (
    CASE
      WHEN (epp.id IS NOT NULL OR epx.id IS NOT NULL) AND epl.id IS NULL THEN 'estimated'
      WHEN epl.id IS NOT NULL AND bp.rebaseline_reason IS NOT NULL AND bp.rebaseline_reason <> 'none' THEN 'remodelled'
      ELSE 'none'
    END
  )
  ELSE 'none'
END`;

/**
 * Annual energy bill (£). New: sum of the individual cost columns on
 * property_baseline_performance (NULL when no baseline row exists, so it's
 * excluded from AVG/SUM rather than counted as £0). Legacy: the old
 * component-sum formula incl. standing charges and the installed-measures
 * adjustment.
 */
export const billsSql = (e: Alias) =>
  sql`CASE WHEN ${isNewApproachSql} THEN (
    CASE WHEN bp.id IS NULL THEN NULL ELSE (
      COALESCE(bp.heating_cost_gbp, 0) +
      COALESCE(bp.hot_water_cost_gbp, 0) +
      COALESCE(bp.lighting_cost_gbp, 0) +
      COALESCE(bp.appliances_cost_gbp, 0) +
      COALESCE(bp.cooking_cost_gbp, 0) +
      COALESCE(bp.pumps_fans_cost_gbp, 0) +
      COALESCE(bp.cooling_cost_gbp, 0) +
      COALESCE(bp.standing_charges_gbp, 0) -
      COALESCE(bp.seg_credit_gbp, 0)
    ) END
  ) ELSE (
    ${e}.heating_cost_current +
    ${e}.hot_water_cost_current +
    ${e}.lighting_cost_current +
    ${e}.appliances_cost_current +
    ${e}.gas_standing_charge +
    ${e}.electricity_standing_charge -
    COALESCE(${e}.installed_measures_total_energy_bill_adjustment, 0)
  ) END`;

/** Total floor area (m²). New: lodged epc_property. Legacy: e.total_floor_area. */
export const totalFloorAreaSql = (e: Alias) =>
  sql`CASE WHEN ${isNewApproachSql} THEN epl.total_floor_area_m2 ELSE ${e}.total_floor_area END`;

/** EPC lodgement date. New: lodged epc_property registration/inspection date. Legacy: e.lodgement_date. */
export const lodgementDateSql = (e: Alias) =>
  sql`CASE WHEN ${isNewApproachSql} THEN COALESCE(epl.registration_date, epl.inspection_date) ELSE ${e}.lodgement_date END`;

/**
 * "estimated" — is this property's EPC picture a gap-fill rather than a real
 * certificate? True for *either* flavour of the predicted slot: a blind
 * `predicted` and a cert-conditioned `expired` are both synthesised, so both
 * carry the Estimated provenance signal.
 *
 * This answers "was it modelled?", NOT "does a certificate exist?" — an `expired`
 * property is estimated AND has an (expired) certificate. For the latter question
 * use `withoutEpcSql`. Conflating the two is what made the reporting cards wrong.
 *
 * Legacy: e.estimated.
 */
export const estimatedSql = (e: Alias) =>
  sql`CASE WHEN ${isNewApproachSql} THEN (epl.id IS NULL AND (epp.id IS NOT NULL OR epx.id IS NOT NULL)) ELSE COALESCE(${e}.estimated, false) END`;

/**
 * "no EPC at all" — we found neither a lodgement record nor a historical one, so
 * nothing has ever been certified for this dwelling and its picture was gap-filled
 * blind. This is the "Homes Without an EPC" card.
 *
 * Note a plain `predicted` row does not make a property "with" an EPC — a
 * prediction is not a certificate. Only `lodged` (a current cert) or `expired` (a
 * historical cert we conditioned on) count as having one. A property with no
 * epc_property rows at all also lands here, which is right: it too has no EPC.
 *
 * Legacy: the flat `estimated` flag, which meant exactly this before the register
 * had a historical tier to distinguish.
 */
export const withoutEpcSql = (e: Alias) =>
  sql`CASE WHEN ${isNewApproachSql} THEN (epl.id IS NULL AND epx.id IS NULL) ELSE COALESCE(${e}.estimated, false) END`;

/**
 * "expired" — we found a certificate, of one kind or the other, and it is out of
 * date. Two ways that happens:
 *
 *   - an `expired` row exists: by construction (Model ADR-0054) it was conditioned
 *     on a pre-2012 certificate, which is >10 years old by definition; or
 *   - the lodged certificate is itself more than 10 years old.
 *
 * The lodged date falls back to `inspection_date` when `registration_date` is null —
 * a sizeable minority of lodged rows carry only the former, and reading
 * registration_date alone silently counted them as current. `lodgementDateSql`
 * above already coalesces the same way.
 *
 * A blindly-`predicted` property is never expired: it has no certificate to expire.
 * Legacy: e.is_expired, less the gap-filled rows (same reason).
 */
export const isExpiredSql = (e: Alias) =>
  sql`CASE WHEN ${isNewApproachSql} THEN (
    epx.id IS NOT NULL
    OR COALESCE(epl.registration_date, epl.inspection_date) < (CURRENT_DATE - INTERVAL '10 years')
  ) ELSE (${e}.is_expired AND NOT COALESCE(${e}.estimated, false)) END`;

/**
 * The main-fuel scalar for one epc_property alias, read from the JSONB
 * `epc_main_heating_detail.main_fuel_type` as a correlated subquery. The stored
 * scalar is either a numeric RdSAP `main_fuel` code ("26", "29", … — ~95% of
 * rows, from the gov-EPC API) or an EPR text string ("Mains gas", "Bulk LPG", …
 * from Elmhurst/PASHub). `#>> '{}'` extracts the JSON scalar as text; NULLIF
 * drops the empty-string sentinel to NULL. Codes/strings are folded to a label
 * downstream by MAINFUEL_OPTIONS — the fragment emits the raw value, like
 * resolvedMainFuelSql's legacy branch. (No index on epc_property_id yet — see
 * ix_epc_main_heating_detail_epc_property in the schema.)
 */
const epcMainFuelSql = (ep: Alias) => sql`(
  SELECT NULLIF(mhd.main_fuel_type #>> '{}', '')
  FROM epc_main_heating_detail mhd
  WHERE mhd.epc_property_id = ${ep}.id
  LIMIT 1
)`;

/**
 * Main fuel raw value. New: the epc_main_heating_detail scalar (lodged over
 * predicted) — an RdSAP code or an EPR string, folded to a label by
 * MAINFUEL_OPTIONS. Legacy: e.mainfuel. Mirrors the new-approach branch of
 * resolveMainFuel.
 */
export const mainfuelSql = (e: Alias) =>
  sql`CASE
    WHEN ${isNewApproachSql} THEN (CASE WHEN epl.id IS NOT NULL THEN ${epcMainFuelSql(sql`epl`)} ELSE ${epcMainFuelSql(sql`epp`)} END)
    ELSE ${e}.mainfuel
  END`;

// ─────────────────────────────────────────────────────────────────────────────
// RdSAP code → label maps for the descriptive fields the new pipeline stores as
// codes on epc_property / epc_building_part (the property-row text columns
// aren't written for new-approach properties). Unknown codes fall back to the
// raw value. Shared by the SQL fragments below and resolvePropertyDescriptors —
// the single source of truth for code→label mapping.
// ─────────────────────────────────────────────────────────────────────────────

// Pure display constants live in @/lib/epc/labels so client bundles can
// import them without pulling in this db-backed module. Imported for local
// use here and re-exported for existing server-side callers.
export { PROPERTY_TYPE_LABELS, BUILT_FORM_LABELS };
// SAP tenure codes → the text labels already used in the data. NOTE: best-effort
// (standard SAP 1/2/3); most rows store the text label directly, only a few use
// codes. Confirm against the backend tenure enum.
const TENURE_LABELS: Record<string, string> = {
  "1": "Owner Occupied",
  "2": "Rented Social",
  "3": "Rented Private",
};
/**
 * Matches the backend ConstructionAgeBand enum (RdSAP England & Wales letters).
 * `year` is a representative construction year (the band's start) for reporting
 * queries that bucket by year — band I (1996–2002) straddles the reporting
 * 1976–1999 / 2000+ boundary, so it buckets with its start year.
 */
export const CONSTRUCTION_AGE_BANDS: Record<
  string,
  { label: string; year: number }
> = {
  A: { label: "before 1900", year: 1899 }, // no start year — any pre-1900 value
  B: { label: "1900–1929", year: 1900 },
  C: { label: "1930–1949", year: 1930 },
  D: { label: "1950–1966", year: 1950 },
  E: { label: "1967–1975", year: 1967 },
  F: { label: "1976–1982", year: 1976 },
  G: { label: "1983–1990", year: 1983 },
  H: { label: "1991–1995", year: 1991 },
  I: { label: "1996–2002", year: 1996 },
  J: { label: "2003–2006", year: 2003 },
  K: { label: "2007–2011", year: 2007 },
  L: { label: "2012–2022", year: 2012 },
  M: { label: "2023 onwards", year: 2023 },
};

/** `CASE <col> WHEN <code> THEN <label> … ELSE <col> END` from a code→label map. */
const codeLabelCaseSql = (col: Alias, labels: Record<string, string>) =>
  sql`CASE ${col} ${sql.join(
    Object.entries(labels).map(([code, label]) => sql`WHEN ${code} THEN ${label}`),
    sql` `,
  )} ELSE ${col} END`;

/** Property-type label from one epc_property alias (mirrors resolvePropertyDescriptors). */
const epcPropertyTypeSql = (ep: Alias) =>
  sql`COALESCE(${codeLabelCaseSql(sql`${ep}.property_type`, PROPERTY_TYPE_LABELS)}, ${ep}.dwelling_type)`;

/**
 * Property type label. New: epc_property (lodged, else predicted) — RdSAP codes
 * mapped via PROPERTY_TYPE_LABELS, text passed through, dwelling_type fallback,
 * mirroring resolvePropertyDescriptors. Legacy: property.property_type.
 */
export const propertyTypeSql = sql`CASE
  WHEN ${isNewApproachSql} THEN (CASE WHEN epl.id IS NOT NULL THEN ${epcPropertyTypeSql(sql`epl`)} ELSE ${epcPropertyTypeSql(sql`epp`)} END)
  ELSE p.property_type
END`;

/**
 * Built-form label. New: epc_property (lodged, else predicted) — RdSAP codes
 * mapped via BUILT_FORM_LABELS, text passed through, mirroring
 * resolvePropertyDescriptors. Legacy: property.built_form.
 */
export const builtFormTypeSql = sql`CASE
  WHEN ${isNewApproachSql} THEN (CASE WHEN epl.id IS NOT NULL THEN ${codeLabelCaseSql(sql`epl.built_form`, BUILT_FORM_LABELS)} ELSE ${codeLabelCaseSql(sql`epp.built_form`, BUILT_FORM_LABELS)} END)
  ELSE p.built_form
END`;

// ─────────────────────────────────────────────────────────────────────────────
// Landlord-override-resolved descriptors (ADR-0012). Each fragment wraps the
// EPC-derived expression above with the property_overrides snapshot and a
// never-null 'Unknown' terminal — the same precedence previewModellingRun
// applies (override → EPC-derived → Unknown). The TS twins in
// descriptorResolution.ts (resolvePropertyType, …) own and unit-test this
// precedence; keep the two in sync.
//
// A resolved fragment references an override alias that its join provides — add
// the matching *OverrideJoin to any query that selects/filters on the fragment.
// Overrides are whole-dwelling for these components, so the join pins
// building_part = 0 (the main building) and never multiplies rows.
// ─────────────────────────────────────────────────────────────────────────────

/** LEFT JOIN exposing the property_type override snapshot (alias `pot`). */
export const propertyTypeOverrideJoin = sql`
  LEFT JOIN property_overrides pot
    ON pot.property_id = p.id
   AND pot.override_component = 'property_type'
   AND pot.building_part = 0
`;

/** property_type: override → EPC-derived → 'Unknown'. Mirrors resolvePropertyType. */
export const resolvedPropertyTypeSql = sql`COALESCE(pot.override_value, ${propertyTypeSql}, 'Unknown')`;

/** LEFT JOIN exposing the built_form_type override snapshot (alias `pobf`). */
export const builtFormOverrideJoin = sql`
  LEFT JOIN property_overrides pobf
    ON pobf.property_id = p.id
   AND pobf.override_component = 'built_form_type'
   AND pobf.building_part = 0
`;

/** built_form: override → EPC-derived → 'Unknown'. Mirrors resolveBuiltForm. */
export const resolvedBuiltFormSql = sql`COALESCE(pobf.override_value, ${builtFormTypeSql}, 'Unknown')`;

/** LEFT JOIN exposing the main_fuel override snapshot (alias `pmf`). */
export const mainFuelOverrideJoin = sql`
  LEFT JOIN property_overrides pmf
    ON pmf.property_id = p.id
   AND pmf.override_component = 'main_fuel'
   AND pmf.building_part = 0
`;

/**
 * main fuel (raw value; folded to a label by the cell's resolveEnumLabel):
 * override → EPC-derived → 'Unknown'. The new EPC graph has no main fuel, so
 * mainfuelSql is NULL for new-approach — the override is the only new-approach
 * source. Takes the legacy `property_details_epc` alias. Mirrors resolveMainFuel.
 */
export const resolvedMainFuelSql = (e: Alias) =>
  sql`COALESCE(pmf.override_value, ${mainfuelSql(e)}, 'Unknown')`;

// ─── Free-text component descriptors: wall / roof / heating (ADR-0012) ────────
// override → EPC-derived (epc_energy_element description, lodged over predicted)
// → legacy property_details_epc column → 'Unknown'. Mirrors resolveOverrideDescriptor.

/** One EPC source's description for an energy element, as a correlated subquery. */
const epcElementDescSql = (ep: Alias, elementType: string) => sql`(
  SELECT ee.description FROM epc_energy_element ee
  WHERE ee.epc_property_id = ${ep}.id AND ee.element_type = ${elementType}
  LIMIT 1
)`;

/** EPC-or-legacy element description: new = lodged over predicted; legacy = the property_details_epc column. */
const epcElementSql = (elementType: string, legacyCol: Alias) => sql`CASE
  WHEN ${isNewApproachSql} THEN (CASE WHEN epl.id IS NOT NULL THEN ${epcElementDescSql(sql`epl`, elementType)} ELSE ${epcElementDescSql(sql`epp`, elementType)} END)
  ELSE ${legacyCol}
END`;

/** LEFT JOIN exposing the wall_type override snapshot at the main building part (alias `pwall`). */
export const wallTypeOverrideJoin = sql`
  LEFT JOIN property_overrides pwall
    ON pwall.property_id = p.id
   AND pwall.override_component = 'wall_type'
   AND pwall.building_part = 0
`;
/** LEFT JOIN exposing the roof_type override snapshot at the main building part (alias `proof`). */
export const roofTypeOverrideJoin = sql`
  LEFT JOIN property_overrides proof
    ON proof.property_id = p.id
   AND proof.override_component = 'roof_type'
   AND proof.building_part = 0
`;
/** LEFT JOIN exposing the main_heating_system override snapshot (alias `pheat`). */
export const mainHeatingOverrideJoin = sql`
  LEFT JOIN property_overrides pheat
    ON pheat.property_id = p.id
   AND pheat.override_component = 'main_heating_system'
   AND pheat.building_part = 0
`;

/** wall type: override → EPC 'wall' description → legacy walls → 'Unknown'. Mirrors resolveOverrideDescriptor. */
export const resolvedWallTypeSql = (e: Alias) =>
  sql`COALESCE(pwall.override_value, ${epcElementSql("wall", sql`${e}.walls`)}, 'Unknown')`;
/** roof type: override → EPC 'roof' description → legacy roof → 'Unknown'. */
export const resolvedRoofTypeSql = (e: Alias) =>
  sql`COALESCE(proof.override_value, ${epcElementSql("roof", sql`${e}.roof`)}, 'Unknown')`;
/** heating system: override → EPC 'main_heating' description → legacy heating → 'Unknown'. */
export const resolvedHeatingSystemSql = (e: Alias) =>
  sql`COALESCE(pheat.override_value, ${epcElementSql("main_heating", sql`${e}.heating`)}, 'Unknown')`;

/**
 * The main building part's construction age band for one epc_property alias, as
 * a correlated scalar subquery — properties can also have extension parts, so a
 * plain JOIN on epc_building_part would multiply rows and break COUNTs.
 */
const mainPartAgeBandSql = (ep: Alias) => sql`(
  SELECT ebp.construction_age_band
  FROM epc_building_part ebp
  WHERE ebp.epc_property_id = ${ep}.id
  ORDER BY CASE WHEN ebp.identifier = 'main' THEN 0 ELSE 1 END, ebp.id
  LIMIT 1
)`;

const ageBandYearCaseSql = (band: Alias) =>
  sql`CASE ${band} ${sql.join(
    Object.entries(CONSTRUCTION_AGE_BANDS).map(
      ([code, { year }]) => sql`WHEN ${code} THEN ${year}::int`,
    ),
    sql` `,
  )} END`;

/**
 * Representative construction year (int) for age-band reporting buckets. New:
 * the main building part's RdSAP construction age band (lodged EPC, else
 * predicted) mapped to the band's start year via CONSTRUCTION_AGE_BANDS.
 * Legacy: property.year_built when numeric. NULL → the caller's Unknown bucket.
 */
export const constructionYearSql = sql`CASE
  WHEN ${isNewApproachSql} THEN (CASE WHEN epl.id IS NOT NULL THEN ${ageBandYearCaseSql(mainPartAgeBandSql(sql`epl`))} ELSE ${ageBandYearCaseSql(mainPartAgeBandSql(sql`epp`))} END)
  WHEN p.year_built ~ '^[0-9]+$' THEN CAST(p.year_built AS int)
  ELSE NULL
END`;

// ─── Construction age as a band LABEL (ADR-0012) ─────────────────────────────
// Unlike constructionYearSql (which flattens the band to a representative year
// for reporting buckets), the portfolio list keeps the band itself. Mirrors
// resolveConstructionAge in descriptorResolution.ts — keep in sync.

/** RdSAP band letter → band label (e.g. B → "1900–1929"). */
const AGE_BAND_LABELS: Record<string, string> = Object.fromEntries(
  Object.entries(CONSTRUCTION_AGE_BANDS).map(([letter, { label }]) => [letter, label]),
);

/** Bucket a legacy year_built expression into its band label; non-numeric → NULL. */
const legacyYearBandLabelSql = (yearExpr: Alias) => {
  const y = sql`CAST(${yearExpr} AS int)`;
  const thresholds = Object.entries(CONSTRUCTION_AGE_BANDS)
    .filter(([letter]) => letter !== "A")
    .sort((a, b) => b[1].year - a[1].year)
    .map(([, { year, label }]) => sql`WHEN ${y} >= ${year} THEN ${label}`);
  // The numeric guard fires the CAST only on digit strings; below the earliest
  // band start falls through to band A.
  return sql`CASE WHEN ${yearExpr} ~ '^[0-9]+$' THEN (CASE ${sql.join(
    thresholds,
    sql` `,
  )} ELSE ${CONSTRUCTION_AGE_BANDS.A.label} END) ELSE NULL END`;
};

/** EPC-or-legacy band label: new-approach main-part band (lodged over predicted); legacy year bucketed. */
const constructionAgeBandLabelSql = sql`CASE
  WHEN ${isNewApproachSql} THEN (CASE WHEN epl.id IS NOT NULL THEN ${codeLabelCaseSql(mainPartAgeBandSql(sql`epl`), AGE_BAND_LABELS)} ELSE ${codeLabelCaseSql(mainPartAgeBandSql(sql`epp`), AGE_BAND_LABELS)} END)
  ELSE ${legacyYearBandLabelSql(sql`p.year_built`)}
END`;

/** LEFT JOIN exposing the construction_age_band override snapshot (alias `pab`). */
export const constructionAgeOverrideJoin = sql`
  LEFT JOIN property_overrides pab
    ON pab.property_id = p.id
   AND pab.override_component = 'construction_age_band'
   AND pab.building_part = 0
`;

/** construction age: override → EPC band → legacy year bucket → 'Unknown'. Mirrors resolveConstructionAge. */
export const resolvedConstructionAgeSql = sql`COALESCE(
  ${codeLabelCaseSql(sql`pab.override_value`, AGE_BAND_LABELS)},
  ${constructionAgeBandLabelSql},
  'Unknown'
)`;

// ─────────────────────────────────────────────────────────────────────────────
// Per-property resolvers (TypeScript object building).
// ─────────────────────────────────────────────────────────────────────────────

/**
 * The subset of the old `PropertyDetailsEpc` shape that the building-passport
 * screens actually consume. Both the legacy row and the new graph map into this.
 */
export interface ConditionReport {
  totalFloorArea: number | null;
  // fabric descriptions + 1–5 ratings
  walls: string | null;
  wallsRating: number | null;
  roof: string | null;
  roofRating: number | null;
  windows: string | null;
  windowsRating: number | null;
  heating: string | null;
  heatingRating: number | null;
  heatingControls: string | null;
  heatingControlsRating: number | null;
  hotWater: string | null;
  hotWaterRating: number | null;
  lighting: string | null;
  lightingRating: number | null;
  floor: string | null;
  floorRating: number | null;
  ventilation: string | null;
  mainfuel: string | null;
  solarPv: number | null;
  solarHotWater: boolean | null;
  windTurbine: number | null;
  // general features
  heatLossCorridor: boolean | null;
  unheatedCorridorLength: number | null;
  numberStoreys: number | null;
  floorHeight: number | null;
  numberHeatedRooms: number | null;
  numberOpenFireplaces: number | null;
  numberExtensions: number | null;
  mainsGas: boolean | null;
  energyTariff: string | null;
  // heat demand + costs
  currentEnergyDemand: number | null;
  primaryEnergyConsumption: number | null;
  co2Emissions: number | null;
  heatingEnergyCostCurrent: number | null;
  hotWaterEnergyCostCurrent: number | null;
  lightingEnergyCostCurrent: number | null;
  appliancesEnergyCostCurrent: number | null;
  // estimated-data banner
  estimated: boolean | null;
}

/** Map an `epc_energy_element.energy_efficiency_rating` to a safe 1–5 (or null). */
function safeRating(r: number | null | undefined): number | null {
  return r != null && r >= 1 && r <= 5 ? r : null;
}

/**
 * Resolve a property's condition report from the new EPC graph. Prefers the
 * lodged EPC; falls back to predicted (in which case `estimated = true`).
 */
async function resolveNewConditionReport(
  propertyId: bigint,
): Promise<ConditionReport | null> {
  const lodged = await db.query.epcProperty.findFirst({
    where: and(
      eq(epcProperty.propertyId, propertyId),
      eq(epcProperty.source, "lodged"),
    ),
  });
  const chosen =
    lodged ??
    (await db.query.epcProperty.findFirst({
      where: and(
        eq(epcProperty.propertyId, propertyId),
        eq(epcProperty.source, "predicted"),
      ),
    }));

  if (!chosen) return null;

  const [elements, flat, baseline, buildingParts] = await Promise.all([
    db.query.epcEnergyElement.findMany({
      where: eq(epcEnergyElement.epcPropertyId, chosen.id),
    }),
    db.query.epcFlatDetails.findFirst({
      where: eq(epcFlatDetails.epcPropertyId, chosen.id),
    }),
    db.query.propertyBaselinePerformance.findFirst({
      where: eq(propertyBaselinePerformance.propertyId, propertyId),
    }),
    db.query.epcBuildingPart.findMany({
      columns: { id: true, identifier: true },
      where: eq(epcBuildingPart.epcPropertyId, chosen.id),
    }),
  ]);

  // Floor height + storey count from the main building part's floor dimensions
  // (fall back to any part). Each floor dimension is one storey of that part.
  const mainPart =
    buildingParts.find((b) => b.identifier === "main") ?? buildingParts[0];
  const mainFloors = mainPart
    ? await db.query.epcFloorDimension.findMany({
        columns: { roomHeightM: true },
        where: eq(epcFloorDimension.epcBuildingPartId, mainPart.id),
      })
    : [];
  const floorHeight = mainFloors[0]?.roomHeightM ?? null;
  const storeyCountFromFloors = mainFloors.length || null;

  const byType = (t: string) => elements.find((el) => el.elementType === t);
  const desc = (t: string) => byType(t)?.description ?? null;
  const rating = (t: string) => safeRating(byType(t)?.energyEfficiencyRating);

  const sumKwh = baseline
    ? (baseline.heatingKwh ?? baseline.spaceHeatingKwh ?? 0) +
      (baseline.hotWaterKwh ?? baseline.waterHeatingKwh ?? 0) +
      (baseline.lightingKwh ?? 0) +
      (baseline.appliancesKwh ?? 0) +
      (baseline.cookingKwh ?? 0) +
      (baseline.pumpsFansKwh ?? 0) +
      (baseline.coolingKwh ?? 0)
    : null;

  return {
    totalFloorArea: chosen.totalFloorAreaM2 ?? null,
    walls: desc("wall"),
    wallsRating: rating("wall"),
    roof: desc("roof"),
    roofRating: rating("roof"),
    windows: desc("window"),
    windowsRating: rating("window"),
    heating: desc("main_heating"),
    heatingRating: rating("main_heating"),
    heatingControls: desc("main_heating_controls"),
    heatingControlsRating: rating("main_heating_controls"),
    hotWater: desc("hot_water"),
    hotWaterRating: rating("hot_water"),
    lighting: desc("lighting"),
    lightingRating: rating("lighting"),
    floor: desc("floor"),
    floorRating: rating("floor"),
    ventilation: chosen.ventilationType ?? null,
    // GAP: no clean main-fuel string in the new graph (jsonb on
    // epc_main_heating_detail) — omitted for new properties.
    mainfuel: null,
    solarPv: chosen.photovoltaicArray ? 1 : 0,
    solarHotWater: chosen.solarWaterHeating ?? null,
    windTurbine: chosen.energyWindTurbinesCount ?? null,
    heatLossCorridor:
      flat?.heatLossCorridor != null ? flat.heatLossCorridor > 0 : null,
    unheatedCorridorLength: flat?.unheatedCorridorLengthM ?? null,
    // Storeys: epc_property value if present, else the main part's floor count.
    numberStoreys: chosen.numberOfStoreys ?? storeyCountFromFloors,
    // Room height of the main building part (the new graph stores height per
    // building part rather than one dwelling value).
    floorHeight,
    numberHeatedRooms: chosen.heatedRoomsCount ?? null,
    numberOpenFireplaces: chosen.openChimneysCount ?? null,
    numberExtensions: chosen.extensionsCount ?? null,
    mainsGas: chosen.energyGasConnectionAvailable ?? null,
    // GAP: no energy tariff in the new graph — omitted.
    energyTariff: null,
    currentEnergyDemand: sumKwh,
    // Effective (re-baselined) baseline so the assessment view agrees with the
    // headline SAP/EPC and the plan maths — see resolvePropertyHeadline.
    primaryEnergyConsumption:
      baseline?.effectivePrimaryEnergyIntensityKwhPerM2Yr ?? null,
    co2Emissions: baseline?.effectiveCo2EmissionsTPerYr ?? null,
    heatingEnergyCostCurrent: baseline?.heatingCostGbp ?? null,
    hotWaterEnergyCostCurrent: baseline?.hotWaterCostGbp ?? null,
    lightingEnergyCostCurrent: baseline?.lightingCostGbp ?? null,
    appliancesEnergyCostCurrent: baseline?.appliancesCostGbp ?? null,
    estimated: !lodged,
  };
}

/** Map a legacy `property_details_epc` row into the `ConditionReport` shape. */
function legacyConditionReport(row: Record<string, unknown>): ConditionReport {
  const r = row as Partial<ConditionReport>;
  return {
    totalFloorArea: r.totalFloorArea ?? null,
    walls: r.walls ?? null,
    wallsRating: r.wallsRating ?? null,
    roof: r.roof ?? null,
    roofRating: r.roofRating ?? null,
    windows: r.windows ?? null,
    windowsRating: r.windowsRating ?? null,
    heating: r.heating ?? null,
    heatingRating: r.heatingRating ?? null,
    heatingControls: r.heatingControls ?? null,
    heatingControlsRating: r.heatingControlsRating ?? null,
    hotWater: r.hotWater ?? null,
    hotWaterRating: r.hotWaterRating ?? null,
    lighting: r.lighting ?? null,
    lightingRating: r.lightingRating ?? null,
    floor: r.floor ?? null,
    floorRating: r.floorRating ?? null,
    ventilation: r.ventilation ?? null,
    mainfuel: r.mainfuel ?? null,
    solarPv: r.solarPv ?? null,
    solarHotWater: r.solarHotWater ?? null,
    windTurbine: r.windTurbine ?? null,
    heatLossCorridor: r.heatLossCorridor ?? null,
    unheatedCorridorLength: r.unheatedCorridorLength ?? null,
    numberStoreys: r.numberStoreys ?? null,
    floorHeight: r.floorHeight ?? null,
    numberHeatedRooms: r.numberHeatedRooms ?? null,
    numberOpenFireplaces: r.numberOpenFireplaces ?? null,
    numberExtensions: r.numberExtensions ?? null,
    mainsGas: r.mainsGas ?? null,
    energyTariff: r.energyTariff ?? null,
    currentEnergyDemand: r.currentEnergyDemand ?? null,
    primaryEnergyConsumption: r.primaryEnergyConsumption ?? null,
    co2Emissions: r.co2Emissions ?? null,
    heatingEnergyCostCurrent: r.heatingEnergyCostCurrent ?? null,
    hotWaterEnergyCostCurrent: r.hotWaterEnergyCostCurrent ?? null,
    lightingEnergyCostCurrent: r.lightingEnergyCostCurrent ?? null,
    appliancesEnergyCostCurrent: r.appliancesEnergyCostCurrent ?? null,
    estimated: r.estimated ?? null,
  };
}

/**
 * Resolve a property's condition report, branching on the cutoff. New-approach
 * properties never touch `property_details_epc`.
 */
export async function resolveConditionReport(
  propertyId: string,
): Promise<ConditionReport> {
  const id = BigInt(propertyId);
  const prop = await db.query.property.findFirst({
    columns: { updatedAt: true },
    where: eq(property.id, id),
  });

  if (prop && isNewApproach(prop.updatedAt)) {
    // Degrade gracefully when the epc_property graph is missing — the screens
    // render "Unknown" rather than 500ing.
    return (await resolveNewConditionReport(id)) ?? legacyConditionReport({});
  }

  const legacy = await db.query.propertyDetailsEpc.findFirst({
    where: eq(propertyDetailsEpc.propertyId, id),
  });
  return legacyConditionReport((legacy ?? {}) as Record<string, unknown>);
}

/**
 * Headline SAP score + EPC band for the property meta. New-approach properties
 * don't have `current_sap_points` / `current_epc_rating` written on the row yet,
 * so we fall back to the modelling baseline (property_baseline_performance).
 * Returns nulls for legacy properties, signalling the caller to keep the row
 * values.
 *
 * Uses `effective_*` — the re-baselined figure the modelling (and therefore the
 * plans) was scored against. The per-property building-passport views (overview,
 * assessment, plans) must agree with the plan maths: re-baselining can move the
 * effective score away from the lodged (gov-registry) EPC, so showing lodged as
 * "current" misrepresents a plan's uplift — e.g. lodged C/78 vs an effective
 * baseline of C/71 with a post-retrofit C/71.4 reads as flat instead of the real
 * improvement. The set-based reporting fragments (`effectiveSapSql`,
 * `carbonSql`, `energyConsumptionSql`, …) read `effective_*` too (ADR-0002);
 * only the dedicated "Lodged EPC" surfaces use the lodged fragments.
 */
export async function resolvePropertyHeadline(
  propertyId: bigint,
  updatedAt: Date | string | null | undefined,
): Promise<{ currentSapPoints: number | null; currentEpcRating: string | null }> {
  if (!isNewApproach(updatedAt)) {
    return { currentSapPoints: null, currentEpcRating: null };
  }
  const baseline = await db.query.propertyBaselinePerformance.findFirst({
    columns: { effectiveSapScore: true, effectiveEpcBand: true },
    where: eq(propertyBaselinePerformance.propertyId, propertyId),
  });
  return {
    currentSapPoints: baseline?.effectiveSapScore ?? null,
    currentEpcRating: baseline?.effectiveEpcBand ?? null,
  };
}

/**
 * Provenance signal + the lodged (gov-register) headline for the EPC card.
 * `provenanceSignal` drives the "estimated"/"re-modelled" UI signals; the lodged
 * band/SAP feed the "Lodged EPC" badge and are only meaningful when a real
 * certificate exists (`source = lodged`). Legacy properties get `none` and null
 * lodged figures — the card falls back to the row's `originalSapPoints`.
 * See provenance.ts (pure precedence), CONTEXT.md and ADR-0002.
 */
export async function resolveProvenance(
  propertyId: bigint,
  updatedAt: Date | string | null | undefined,
): Promise<{
  provenanceSignal: ProvenanceSignal;
  lodgedEpcRating: string | null;
  lodgedSapPoints: number | null;
}> {
  if (!isNewApproach(updatedAt)) {
    return { provenanceSignal: "none", lodgedEpcRating: null, lodgedSapPoints: null };
  }
  const [lodged, predicted, baseline] = await Promise.all([
    db.query.epcProperty.findFirst({
      columns: { id: true },
      where: and(eq(epcProperty.propertyId, propertyId), eq(epcProperty.source, "lodged")),
    }),
    db.query.epcProperty.findFirst({
      columns: { id: true },
      where: and(eq(epcProperty.propertyId, propertyId), eq(epcProperty.source, "predicted")),
    }),
    db.query.propertyBaselinePerformance.findFirst({
      columns: { rebaselineReason: true, lodgedSapScore: true, lodgedEpcBand: true },
      where: eq(propertyBaselinePerformance.propertyId, propertyId),
    }),
  ]);

  const hasLodgedEpc = !!lodged;
  const provenanceSignal = resolveProvenanceSignal({
    isNewApproach: true,
    hasLodgedEpc,
    hasPredictedEpc: !!predicted,
    rebaselineReason: baseline?.rebaselineReason,
  });

  // Lodged figures only when a real certificate exists — a predicted property
  // still carries lodged_* (mirrored estimates) which must not surface.
  return {
    provenanceSignal,
    lodgedEpcRating: hasLodgedEpc ? baseline?.lodgedEpcBand ?? null : null,
    lodgedSapPoints: hasLodgedEpc ? baseline?.lodgedSapScore ?? null : null,
  };
}

/** The minimal EPC meta the `/api/property-meta` route embeds as `detailsEpc`. */
export interface DetailsEpcMeta {
  currentEnergyDemand: number | null;
  co2Emissions: number | null;
  estimated: boolean;
}

/** Resolve the `detailsEpc` meta block, branching on the cutoff. */
export async function resolveDetailsEpcMeta(
  propertyId: bigint,
  updatedAt: Date | string | null | undefined,
): Promise<DetailsEpcMeta> {
  if (isNewApproach(updatedAt)) {
    const [lodged, predicted, baseline] = await Promise.all([
      db.query.epcProperty.findFirst({
        columns: { id: true },
        where: and(
          eq(epcProperty.propertyId, propertyId),
          eq(epcProperty.source, "lodged"),
        ),
      }),
      db.query.epcProperty.findFirst({
        columns: { id: true },
        where: and(
          eq(epcProperty.propertyId, propertyId),
          eq(epcProperty.source, "predicted"),
        ),
      }),
      db.query.propertyBaselinePerformance.findFirst({
        where: eq(propertyBaselinePerformance.propertyId, propertyId),
      }),
    ]);

    const currentEnergyDemand = baseline
      ? (baseline.heatingKwh ?? baseline.spaceHeatingKwh ?? 0) +
        (baseline.hotWaterKwh ?? baseline.waterHeatingKwh ?? 0) +
        (baseline.lightingKwh ?? 0) +
        (baseline.appliancesKwh ?? 0)
      : null;

    return {
      currentEnergyDemand,
      co2Emissions: baseline?.effectiveCo2EmissionsTPerYr ?? null,
      estimated: !lodged && !!predicted,
    };
  }

  const legacy = await db.query.propertyDetailsEpc.findFirst({
    columns: { currentEnergyDemand: true, co2Emissions: true, estimated: true },
    where: eq(propertyDetailsEpc.propertyId, propertyId),
  });
  return {
    currentEnergyDemand: legacy?.currentEnergyDemand ?? null,
    co2Emissions: legacy?.co2Emissions ?? null,
    estimated: legacy?.estimated ?? false,
  };
}

/**
 * Resolve the descriptive property fields (type / built form / age) from the
 * epc_property graph for new-approach properties, since the property-row text
 * columns aren't written. Returns nulls when there's no EPC graph.
 */
async function resolvePropertyDescriptors(
  propertyId: bigint,
): Promise<{
  propertyType: string | null;
  builtForm: string | null;
  yearBuilt: string | null;
  numberOfRooms: number | null;
  tenure: string | null;
}> {
  const cols = {
    id: true,
    propertyType: true,
    builtForm: true,
    dwellingType: true,
    habitableRoomsCount: true,
    tenure: true,
  } as const;
  const ep =
    (await db.query.epcProperty.findFirst({
      columns: cols,
      where: and(
        eq(epcProperty.propertyId, propertyId),
        eq(epcProperty.source, "lodged"),
      ),
    })) ??
    (await db.query.epcProperty.findFirst({
      columns: cols,
      where: and(
        eq(epcProperty.propertyId, propertyId),
        eq(epcProperty.source, "predicted"),
      ),
    }));

  if (!ep) {
    return {
      propertyType: null,
      builtForm: null,
      yearBuilt: null,
      numberOfRooms: null,
      tenure: null,
    };
  }

  const mainPart = await db.query.epcBuildingPart.findFirst({
    columns: { constructionAgeBand: true },
    where: eq(epcBuildingPart.epcPropertyId, ep.id),
  });

  // property_type / built_form are stored either as numeric RdSAP codes (newer
  // rows) or as plain text ("House", "Detached" — most rows). Map numeric codes;
  // pass text through unchanged; fall back to dwelling_type for property type.
  const propertyType = ep.propertyType
    ? PROPERTY_TYPE_LABELS[ep.propertyType] ?? ep.propertyType
    : ep.dwellingType ?? null;
  const builtForm = ep.builtForm
    ? BUILT_FORM_LABELS[ep.builtForm] ?? ep.builtForm
    : null;
  const ageBand = mainPart?.constructionAgeBand ?? null;
  const yearBuilt = ageBand
    ? CONSTRUCTION_AGE_BANDS[ageBand]?.label ?? ageBand
    : null;

  const tenure = ep.tenure
    ? TENURE_LABELS[ep.tenure] ?? ep.tenure
    : null;

  return {
    propertyType,
    builtForm,
    yearBuilt,
    numberOfRooms: ep.habitableRoomsCount ?? null,
    tenure,
  };
}

/**
 * Resolve the full property-meta object (the property row + branched
 * `detailsEpc` + headline SAP/EPC). Shared by the `/api/property-meta` route and
 * the building-passport `getPropertyMeta` so both run identical, in-process
 * logic — no self-fetch to `process.env.URL` (which could hit a different
 * deployment) and no Data Cache. Returns null when the property doesn't exist.
 * BigInt fields are left raw; the caller serialises with `serializeBigInt`.
 */
export async function resolvePropertyMeta(propertyId: string) {
  const propertyMeta = await db.query.property.findFirst({
    columns: {
      id: true,
      uprn: true,
      address: true,
      postcode: true,
      hasPreConditionReport: true,
      hasRecommendations: true,
      createdAt: true,
      propertyType: true,
      builtForm: true,
      localAuthority: true,
      constituency: true,
      numberOfRooms: true,
      yearBuilt: true,
      tenure: true,
      currentEpcRating: true,
      currentSapPoints: true,
      originalSapPoints: true,
      updatedAt: true,
      currentValuation: true,
    },
    where: eq(property.id, BigInt(propertyId)),
  });

  if (!propertyMeta) return null;

  const newApproach = isNewApproach(propertyMeta.updatedAt);
  const [detailsEpc, headline, descriptors, provenance] = await Promise.all([
    resolveDetailsEpcMeta(propertyMeta.id, propertyMeta.updatedAt),
    resolvePropertyHeadline(propertyMeta.id, propertyMeta.updatedAt),
    newApproach
      ? resolvePropertyDescriptors(propertyMeta.id)
      : Promise.resolve(null),
    resolveProvenance(propertyMeta.id, propertyMeta.updatedAt),
  ]);

  return {
    ...propertyMeta,
    provenanceSignal: provenance.provenanceSignal,
    lodgedEpcRating: provenance.lodgedEpcRating,
    lodgedSapPoints: provenance.lodgedSapPoints,
    propertyType: descriptors?.propertyType ?? propertyMeta.propertyType,
    builtForm: descriptors?.builtForm ?? propertyMeta.builtForm,
    yearBuilt: descriptors?.yearBuilt ?? propertyMeta.yearBuilt,
    numberOfRooms: descriptors?.numberOfRooms ?? propertyMeta.numberOfRooms,
    tenure: descriptors?.tenure ?? propertyMeta.tenure,
    currentSapPoints:
      headline.currentSapPoints ?? propertyMeta.currentSapPoints,
    currentEpcRating:
      headline.currentEpcRating ?? propertyMeta.currentEpcRating,
    detailsEpc,
  };
}
