/**
 * Modelling-run domain model — pure logic for the bulk-trigger-modelling
 * journey. See CONTEXT.md (Modelling run, Run filter) and ADR-0008.
 */

import { normalisePostcode } from "@/lib/postcodeSearch/model";
import {
  BuiltFormTypeValues,
  PropertyTypeValues,
} from "@/app/db/schema/landlord_overrides";

/**
 * The Run-filter vocabularies: the canonical enum lists, which already end in
 * "Unknown" — the no-resolvable-value bucket (CONTEXT.md, Run filter).
 */
export const RUN_FILTER_PROPERTY_TYPES: readonly string[] = PropertyTypeValues;
export const RUN_FILTER_BUILT_FORMS: readonly string[] = BuiltFormTypeValues;

/** An absent key means unconstrained — "all" is never an enumeration. */
export interface RunFilters {
  postcodes?: string[];
  propertyTypes?: string[];
  builtForms?: string[];
  /** Portfolio Tag ids (any-of); resolved against property_tag (ADR-0013). */
  tagIds?: number[];
}

export type NormaliseFiltersResult =
  | { ok: true; filters: RunFilters }
  | { ok: false; error: string };

/** UX guard, not a transport limit — the config is stored, not carried. */
export const MAX_FILTER_POSTCODES = 40;

export function normaliseRunFilters(input: {
  postcodes?: string[];
  propertyTypes?: string[];
  builtForms?: string[];
  tagIds?: (number | string)[];
}): NormaliseFiltersResult {
  const filters: RunFilters = {};
  if (input.postcodes?.length) {
    const seen = new Set<string>();
    for (const raw of input.postcodes) {
      const pc = normalisePostcode(raw);
      if (!pc) return { ok: false, error: `Invalid postcode '${raw}'` };
      seen.add(pc);
    }
    filters.postcodes = [...seen].sort();
    if (filters.postcodes.length > MAX_FILTER_POSTCODES) {
      return {
        ok: false,
        error: `Filters are limited to ${MAX_FILTER_POSTCODES} postcodes per run — leave the filter empty to include all postcodes`,
      };
    }
  }
  // The filter vocabularies are the canonical enum lists (which already end
  // in "Unknown" — the no-resolvable-value bucket, see CONTEXT.md Run filter).
  for (const [key, allowed] of [
    ["propertyTypes", PropertyTypeValues],
    ["builtForms", BuiltFormTypeValues],
  ] as const) {
    const values = input[key];
    if (!values?.length) continue;
    const bad = values.find((v) => !allowed.includes(v));
    if (bad !== undefined) {
      return { ok: false, error: `Invalid ${key === "propertyTypes" ? "property type" : "built form"} '${bad}'` };
    }
    filters[key] = [...new Set(values)];
  }
  // Tags are dynamic (portfolio-specific), so there's no vocabulary to check
  // against — just coerce to positive integer ids, dedupe and sort. Ids that
  // don't belong to the portfolio simply match nothing at resolution time.
  if (input.tagIds?.length) {
    const ids = new Set<number>();
    for (const raw of input.tagIds) {
      const n = Number(raw);
      if (!Number.isInteger(n) || n <= 0) {
        return { ok: false, error: `Invalid tag '${raw}'` };
      }
      ids.add(n);
    }
    filters.tagIds = [...ids].sort((a, b) => a - b);
  }
  return { ok: true, filters };
}

export interface RunConfigView {
  scenarioIds: string[];
  filters: RunFilters;
  previewedPropertyCount: number;
  triggeredBy: string;
}

/**
 * Read a stored config-subtask `inputs` back into the app's shape. Null for
 * anything that isn't a modelling-run config (malformed, legacy, absent) —
 * history reads must never throw on old rows.
 */
export function parseRunConfig(inputs: string | null): RunConfigView | null {
  if (!inputs) return null;
  try {
    const raw = JSON.parse(inputs) as Partial<RunConfigInputs>;
    if (!Array.isArray(raw.scenario_ids) || typeof raw.triggered_by !== "string") {
      return null;
    }
    const filters: RunFilters = {};
    if (raw.filters?.postcodes?.length) filters.postcodes = raw.filters.postcodes;
    if (raw.filters?.property_types?.length) filters.propertyTypes = raw.filters.property_types;
    if (raw.filters?.built_forms?.length) filters.builtForms = raw.filters.built_forms;
    if (raw.filters?.tag_ids?.length) filters.tagIds = raw.filters.tag_ids;
    return {
      scenarioIds: raw.scenario_ids.map(String),
      filters,
      previewedPropertyCount: raw.previewed_property_count ?? 0,
      triggeredBy: raw.triggered_by,
    };
  } catch {
    return null;
  }
}

/** Above this, the UI asks for an explicit extra confirmation before running. */
export const LARGE_RUN_THRESHOLD = 10_000;

export function isLargeRun(matchedPropertyCount: number): boolean {
  return matchedPropertyCount >= LARGE_RUN_THRESHOLD;
}

export type RunStatus =
  | { state: "dispatched" }
  | { state: "in_progress"; ready: number; total: number }
  | { state: "complete" }
  | { state: "failed" };

/**
 * Display state of a run from its task + execution-subtask counts (the config
 * subtask is excluded by the caller). Status strings follow the task system's
 * loose conventions (case-insensitive; completion also signalled by
 * job_completed).
 */
export function deriveRunStatus(input: {
  taskStatus: string;
  jobCompleted: Date | null;
  total: number;
  completed: number;
  failed: number;
}): RunStatus {
  const s = input.taskStatus.toLowerCase();
  if (["failed", "failure", "error"].includes(s)) return { state: "failed" };
  if (input.jobCompleted || ["completed", "complete"].includes(s)) {
    return { state: "complete" };
  }
  if (input.total === 0) return { state: "dispatched" };
  return { state: "in_progress", ready: input.completed, total: input.total };
}

/**
 * Rough results ETA shown after a run is triggered: the distributor works
 * through ~300 plans per 5-minute batch window, floored at 5 minutes so a
 * tiny run never promises the impossible. A hedge ("typically ready
 * within…"), not a contract.
 */
export function estimateRunMinutes(planCount: number): number {
  return Math.max(5, Math.round(planCount / 300) * 5);
}

/**
 * Run-history poll cadence: fast while any run is still moving so progress
 * feels live, slow once everything has settled. Shape matches TanStack v4's
 * refetchInterval callback, which receives the query data first.
 */
export function historyRefetchInterval(
  runs: { status: RunStatus }[] | undefined,
): number {
  const active = runs?.some(
    (r) => r.status.state === "dispatched" || r.status.state === "in_progress",
  );
  return active ? 5_000 : 30_000;
}

/** Human summary of a Run filter, as shown in run history ("All properties"). */
export function selectionSummary(filters: RunFilters): string {
  const bits: string[] = [];
  if (filters.postcodes?.length) bits.push(`Postcodes: ${filters.postcodes.join(", ")}`);
  if (filters.propertyTypes?.length) bits.push(`Type: ${filters.propertyTypes.join(", ")}`);
  if (filters.builtForms?.length) bits.push(`Built form: ${filters.builtForms.join(", ")}`);
  // Tag names aren't carried in the pure filters (only ids), so summarise as a
  // count; the run-history UI resolves names separately when it needs them.
  if (filters.tagIds?.length) bits.push(`Tags: ${filters.tagIds.length}`);
  return bits.length ? bits.join(" · ") : "All properties";
}

export interface RunRequest {
  portfolioId: string;
  scenarioIds: string[];
  filters: RunFilters;
  previewedPropertyCount: number;
  triggeredBy: string;
}

/** Backend-facing (snake_case) copy of a run's configuration. */
export interface RunConfigInputs {
  portfolio_id: number;
  scenario_ids: number[];
  filters: {
    postcodes?: string[];
    property_types?: string[];
    built_forms?: string[];
    tag_ids?: number[];
  };
  previewed_property_count: number;
  triggered_by: string;
}

/**
 * The row a Modelling run persists (ADR-0008): an app-authored task whose
 * `inputs` carries the run's config — the durable, history-readable record.
 * Sub-tasks are the distributor's: per-property execution work.
 */
export function buildRunRecord(req: RunRequest): {
  task: {
    taskSource: string;
    service: "modelling_run";
    source: "portfolio_id";
    sourceId: string;
    status: "waiting";
    inputs: string;
  };
  /** What the distributor receives — provenance fields stay on the task. */
  dispatchPayload: Omit<RunConfigInputs, "previewed_property_count" | "triggered_by">;
} {
  const filters: RunConfigInputs["filters"] = {};
  if (req.filters.postcodes) filters.postcodes = req.filters.postcodes;
  if (req.filters.propertyTypes) filters.property_types = req.filters.propertyTypes;
  if (req.filters.builtForms) filters.built_forms = req.filters.builtForms;
  if (req.filters.tagIds) filters.tag_ids = req.filters.tagIds;
  const configInputs: RunConfigInputs = {
    portfolio_id: Number(req.portfolioId),
    scenario_ids: req.scenarioIds.map(Number),
    filters,
    previewed_property_count: req.previewedPropertyCount,
    triggered_by: req.triggeredBy,
  };
  return {
    task: {
      taskSource: `Modelling run – ${req.scenarioIds.length} scenario${req.scenarioIds.length === 1 ? "" : "s"}`,
      service: "modelling_run",
      source: "portfolio_id",
      sourceId: req.portfolioId,
      status: "waiting",
      inputs: JSON.stringify(configInputs),
    },
    dispatchPayload: {
      portfolio_id: configInputs.portfolio_id,
      scenario_ids: configInputs.scenario_ids,
      filters,
    },
  };
}
