// View-model logic for the portfolio home page: pure functions the client
// components derive their render state from. Kept free of React and drizzle
// so it tests in node and survives refactors on either side.

export type StatusTone =
  | "early"
  | "tender"
  | "underway"
  | "ontrack"
  | "delayed"
  | "atrisk"
  | "done"
  | "review";

export type StatusDisplay = {
  label: string;
  tone: StatusTone;
  needsAttention: boolean;
};

// The lifecycle enum's raw values ("completion; status: at risk") are storage
// strings, not UI copy; this is the single place they become pill text.
const STATUS_DISPLAY: Record<string, StatusDisplay> = {
  scoping: { label: "Scoping", tone: "early", needsAttention: false },
  survey: { label: "Survey", tone: "early", needsAttention: false },
  assessment: { label: "Assessment", tone: "early", needsAttention: false },
  tendering: { label: "Tendering", tone: "tender", needsAttention: false },
  "project underway": {
    label: "Underway",
    tone: "underway",
    needsAttention: false,
  },
  "completion; status: on track": {
    label: "On track",
    tone: "ontrack",
    needsAttention: false,
  },
  "completion; status: delayed": {
    label: "Delayed",
    tone: "delayed",
    needsAttention: true,
  },
  "completion; status: at risk": {
    label: "At risk",
    tone: "atrisk",
    needsAttention: true,
  },
  "completion; status: completed": {
    label: "Completed",
    tone: "done",
    needsAttention: false,
  },
  "needs review": { label: "Needs review", tone: "review", needsAttention: true },
};

const UNKNOWN_STATUS: StatusDisplay = {
  label: "Unknown",
  tone: "early",
  needsAttention: false,
};

export function statusDisplay(status: string): StatusDisplay {
  return STATUS_DISPLAY[status] ?? UNKNOWN_STATUS;
}

export type HomeSort = "updated" | "attention" | "name" | "props";

// Wire-format rows (ids and dates as strings — see the portfolio-home
// route's bigint serialisation).
export type LocalConfig = {
  portfolioId: string;
  folderId: string | null;
  starredAt: string | null;
};

export type PortfolioWire = {
  id: string;
  name: string;
  status: string;
  goal: string;
  budget: number | null;
  cost: number | null;
  numberOfProperties: number | null;
  updatedAt: string;
};

export type FolderWire = { id: string; name: string; position: number };

export type HomeData = {
  portfolios: PortfolioWire[];
  configs: LocalConfig[];
  folders: FolderWire[];
};

// Client-side mirror of the lazy DB upsert, used for optimistic cache
// updates: no row yet means "all defaults", and a change touches only the
// fields it names.
export function upsertLocalConfig(
  configs: LocalConfig[],
  portfolioId: string,
  change: Partial<Pick<LocalConfig, "folderId" | "starredAt">>,
): LocalConfig[] {
  const existing = configs.find((c) => c.portfolioId === portfolioId);
  if (!existing) {
    return [
      ...configs,
      { portfolioId, folderId: null, starredAt: null, ...change },
    ];
  }
  return configs.map((c) =>
    c.portfolioId === portfolioId ? { ...c, ...change } : c,
  );
}

// Severity for "needs attention first": worst state leads, healthy and
// finished work trails.
const ATTENTION_RANK: Record<StatusTone, number> = {
  atrisk: 0,
  delayed: 1,
  review: 2,
  underway: 3,
  tender: 4,
  early: 5,
  ontrack: 6,
  done: 7,
};

export function sortPortfolios<
  P extends {
    name: string;
    status: string;
    numberOfProperties: number | null;
    updatedAt: Date | string;
  },
>(portfolios: P[], sort: HomeSort): P[] {
  const by: Record<HomeSort, (a: P, b: P) => number> = {
    updated: (a, b) =>
      new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
    attention: (a, b) =>
      ATTENTION_RANK[statusDisplay(a.status).tone] -
      ATTENTION_RANK[statusDisplay(b.status).tone],
    name: (a, b) => a.name.localeCompare(b.name),
    props: (a, b) => (b.numberOfProperties ?? 0) - (a.numberOfProperties ?? 0),
  };
  return [...portfolios].sort(by[sort]);
}

// Users search in display language ("at risk"), never in enum storage
// strings, so matching goes through statusDisplay.
export function matchesSearch(
  portfolio: {
    name: string;
    status: string;
    goal: string;
    folderName: string | null;
  },
  query: string,
): boolean {
  const q = query.trim().toLowerCase();
  if (!q) return true;
  return [
    portfolio.name,
    statusDisplay(portfolio.status).label,
    portfolio.goal,
    portfolio.folderName ?? "",
  ]
    .join(" ")
    .toLowerCase()
    .includes(q);
}

export function formatUpdatedAgo(updatedAt: string, now: Date): string {
  const days = Math.floor(
    (now.getTime() - new Date(updatedAt).getTime()) / 86_400_000,
  );
  if (days <= 0) return "today";
  if (days === 1) return "yesterday";
  if (days < 28) return `${days} days ago`;
  const months = Math.round(days / 30);
  return months === 1 ? "1 month ago" : `${months} months ago`;
}

export function formatMoney(amount: number): string {
  if (amount >= 1_000_000) {
    return "£" + (amount / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
  }
  if (amount >= 1_000) {
    return "£" + Math.round(amount / 1_000) + "k";
  }
  return "£" + Math.round(amount);
}
