/**
 * Scenario domain model — the pure logic behind the Scenarios tab.
 *
 * See CONTEXT.md (Scenario, Goal, Budget, Measure exclusion) and ADR-0003:
 * a Scenario's configuration is immutable once created; the name is a
 * renameable label; goal semantics are "Increasing EPC" targets a band while
 * every other goal maximises within an optional per-property budget.
 */

import { PORTFOLIO_GOALS } from "@/app/db/schema/portfolio";
import { HousingType } from "@/app/db/schema/recommendations";

/**
 * The measures a Scenario may exclude — EXACTLY the backend's `MeasureType`
 * values (Hestia-Homes/Model, domain/modelling/measure_type.py). The engine's
 * exclusion parsing is strict: a token outside this set crashes the modelling
 * run for every property in the scenario, so this list and the backend enum
 * must change together (the backend's ADR-0056 shared-contract rule; pinned
 * by a test). Tokens are the backend's; labels are ours.
 *
 * Deliberately narrower than the legacy `measuresDisplayLabels` vocabulary
 * (recommendations display / the old file-based trigger): measures the new
 * engine cannot recommend (room-in-roof insulation, fireplace sealing,
 * hot-water tank insulation, cylinder thermostats) are not offered —
 * excluding the unrecommendable is meaningless, and storing the token is a
 * time bomb. Room-in-roof insulation returns here when the backend
 * implements it.
 */
export const SCENARIO_MEASURES = {
  cavity_wall_insulation: "Cavity Wall Insulation",
  external_wall_insulation: "External Wall Insulation",
  internal_wall_insulation: "Internal Wall Insulation",
  loft_insulation: "Loft Insulation",
  sloping_ceiling_insulation: "Sloping Ceiling Insulation",
  flat_roof_insulation: "Flat Roof Insulation",
  suspended_floor_insulation: "Suspended Floor Insulation",
  solid_floor_insulation: "Solid Floor Insulation",
  double_glazing: "Double Glazing",
  secondary_glazing: "Secondary Glazing",
  low_energy_lighting: "Low Energy Lighting",
  mechanical_ventilation: "Mechanical Ventilation",
  high_heat_retention_storage_heaters: "High Heat Retention Storage Heaters",
  air_source_heat_pump: "Air Source Heat Pump",
  gas_boiler_upgrade: "Gas Boiler Upgrade",
  system_tune_up: "Heating System Tune-up",
  system_tune_up_zoned: "Heating System Tune-up (zoned)",
  solar_pv: "Solar PV",
  secondary_heating_removal: "Secondary Heating Removal",
} as const;

export type ScenarioMeasureKey = keyof typeof SCENARIO_MEASURES;

export const scenarioMeasuresList = Object.keys(
  SCENARIO_MEASURES,
) as ScenarioMeasureKey[];

export const EPC_BANDS = ["A", "B", "C", "D", "E", "F", "G"] as const;
export type EpcBand = (typeof EPC_BANDS)[number];

/**
 * Goals a user may author a Scenario with. "None" exists in the portfolio
 * goal enum but is not a modelling objective, so it is not offered.
 */
export const SCENARIO_GOALS = [
  PORTFOLIO_GOALS.EPC,
  PORTFOLIO_GOALS.CO2,
  PORTFOLIO_GOALS.ENERGY,
  PORTFOLIO_GOALS.VALUATION,
] as const;

export interface ScenarioConfigInput {
  name: string;
  housingType: string;
  goal: string;
  goalValue: string | null;
  budgetPerProperty: number | null;
  exclusions: string[];
  /** Fabric-before-heating ordering constraint (CONTEXT.md). Off by default. */
  fabricFirst?: boolean;
}

export type ValidationResult =
  | { ok: true; value: ScenarioConfigInput }
  | { ok: false; errors: Partial<Record<keyof ScenarioConfigInput, string>> };

export function validateScenarioConfig(
  input: ScenarioConfigInput,
): ValidationResult {
  const errors: Partial<Record<keyof ScenarioConfigInput, string>> = {};

  if (!(SCENARIO_GOALS as readonly string[]).includes(input.goal)) {
    errors.goal = `Goal must be one of: ${SCENARIO_GOALS.join(", ")}`;
  }

  if (!HousingType.includes(input.housingType)) {
    errors.housingType = `Housing type must be one of: ${HousingType.join(", ")}`;
  }

  const name = input.name.trim();
  if (!name) {
    errors.name = "Give the scenario a name";
  }

  if (input.budgetPerProperty !== null && !(input.budgetPerProperty > 0)) {
    errors.budgetPerProperty =
      "Budget per property must be a positive amount, or empty for no cap";
  }

  const isEpcGoal = input.goal === PORTFOLIO_GOALS.EPC;
  let goalValue: string | null = null;
  if (isEpcGoal) {
    if (!input.goalValue || !EPC_BANDS.includes(input.goalValue as EpcBand)) {
      errors.goalValue = "A target EPC band (A–G) is required for this goal";
    } else {
      goalValue = input.goalValue;
    }
  }
  // Non-EPC goals are "improve as much as possible" — they carry no goal
  // value (any stray input is dropped); the only brake is the budget.

  const exclusions = [...new Set(input.exclusions)].sort();
  const unknown = exclusions.filter(
    (k) => !(scenarioMeasuresList as string[]).includes(k),
  );
  if (unknown.length > 0) {
    errors.exclusions = `Unknown measures: ${unknown.join(", ")}`;
  } else if (exclusions.length === scenarioMeasuresList.length) {
    errors.exclusions =
      "Every measure is excluded — the engine would have nothing to work with. Allow at least one.";
  }

  if (Object.keys(errors).length > 0) return { ok: false, errors };
  return {
    ok: true,
    value: { ...input, name, goalValue, exclusions, fabricFirst: input.fabricFirst ?? false },
  };
}

/**
 * `scenario.exclusions` is a text column holding the backend's brace format,
 * e.g. "{loft_insulation,solid_floor_insulation}" — some legacy rows pad
 * entries with spaces. Empty set is stored as NULL (matching rows the
 * backend writes with no exclusions).
 */
export function serializeExclusions(keys: string[]): string | null {
  if (keys.length === 0) return null;
  return "{" + keys.join(",") + "}";
}

/**
 * Map a validated config to the `scenario` insert row per ADR-0003:
 * app-authored rows always set multiPlan=true (universal in modern data) and
 * isDefault=false (the backend owns default-scenario semantics at modelling
 * time), and never touch the legacy aggregate columns.
 */
export function scenarioToInsertRow(
  value: ScenarioConfigInput,
  portfolioId: bigint,
) {
  return {
    portfolioId,
    name: value.name,
    housingType: value.housingType,
    goal: value.goal,
    goalValue: value.goalValue,
    budget: value.budgetPerProperty,
    exclusions: serializeExclusions(value.exclusions),
    multiPlan: true,
    isDefault: false,
    fabricFirst: value.fabricFirst ?? false,
  };
}

export function parseExclusions(text: string | null): string[] {
  if (!text) return [];
  const inner = text.trim().replace(/^\{/, "").replace(/\}$/, "");
  if (!inner.trim()) return [];
  return inner.split(",").map((s) => s.trim()).filter(Boolean);
}

/**
 * Measures whose installation involves major internal works — landlords
 * frequently exclude them. The value is the plain-language reason shown to
 * users. The "Low disruption" quick-start excludes exactly this set.
 */
export const DISRUPTIVE_MEASURES: Partial<Record<ScenarioMeasureKey, string>> = {
  internal_wall_insulation:
    "Rooms need clearing and replastering while the work is done",
  solid_floor_insulation:
    "Floors are dug up or raised — usually needs the home empty",
  suspended_floor_insulation:
    "Floorboards come up in every affected room",
  // room-in-roof insulation belongs here ("loft rooms are stripped back to
  // the rafters") but is not yet a backend MeasureType — restore it when the
  // backend implements the measure.
};

export type ScenarioStatus = "modelled" | "awaiting_modelling";

/** Status is derived from plan existence (ADR-0003) — never stored. */
export function deriveScenarioStatus(plansCount: number): ScenarioStatus {
  return plansCount > 0 ? "modelled" : "awaiting_modelling";
}

export type ConstraintSummary =
  | { kind: "all" }
  | { kind: "excludes"; measures: string[] }
  | { kind: "only"; measures: string[] };

/**
 * Frame a scenario's measure constraints from whichever side is shorter:
 * few exclusions → "Excludes …"; nearly-all excluded (≤3 allowed) → "Only …".
 * Returns display labels, in the canonical measuresList order.
 */
export function constraintSummary(exclusions: string[]): ConstraintSummary {
  if (exclusions.length === 0) return { kind: "all" };
  const excluded = new Set(exclusions);
  const allowed = scenarioMeasuresList.filter((k) => !excluded.has(k));
  if (allowed.length <= 3) {
    return { kind: "only", measures: allowed.map((k) => SCENARIO_MEASURES[k]) };
  }
  const known = scenarioMeasuresList
    .filter((k) => excluded.has(k))
    .map((k) => SCENARIO_MEASURES[k]);
  // Legacy rows may hold tokens outside today's vocabulary — display them
  // as themselves rather than dropping them silently.
  const legacy = exclusions.filter(
    (k) => !(scenarioMeasuresList as string[]).includes(k),
  );
  return { kind: "excludes", measures: [...known, ...legacy] };
}

/** The stored shape a duplicate check compares against (subset of the scenario row). */
export interface ExistingScenarioConfig {
  id: bigint;
  name: string | null;
  housingType: string;
  goal: string;
  goalValue: string | null;
  budget: number | null;
  exclusions: string | null;
  fabricFirst: boolean;
}

/**
 * Exact-configuration duplicate: same goal, goal value, housing type, budget,
 * exclusion set and fabric-first flag. The name is a label and is deliberately
 * ignored — an identically-configured scenario would model to identical plans,
 * so any facet that changes the modelled outcome (fabric-first among them)
 * makes it a distinct question.
 */
export function findExactDuplicate(
  config: Pick<
    ScenarioConfigInput,
    "housingType" | "goal" | "goalValue" | "budgetPerProperty" | "exclusions" | "fabricFirst"
  >,
  existing: ExistingScenarioConfig[],
): ExistingScenarioConfig | null {
  const wanted = [...new Set(config.exclusions)].sort().join(",");
  return (
    existing.find(
      (s) =>
        s.goal === config.goal &&
        (s.goalValue ?? null) === (config.goalValue ?? null) &&
        s.housingType === config.housingType &&
        (s.budget ?? null) === (config.budgetPerProperty ?? null) &&
        (s.fabricFirst ?? false) === (config.fabricFirst ?? false) &&
        parseExclusions(s.exclusions).sort().join(",") === wanted,
    ) ?? null
  );
}
