import type { FeatureKey } from "../entitlements";

export interface ImportSource {
  id: number;
  name: string;
  description?: string;
  date_format: string;
  delimiter: string;
  encoding: string;
  column_mapping: string;
  skip_lines: number;
  has_header: boolean;
  /**
   * The two fields that decide how an amount is READ, added by migration v17.
   * Before v17 they lived only on `import_config_templates`, so restoring a
   * source re-inferred the mode and hardcoded the convention (#324).
   *
   * They are never read directly: `formatFromRow` is the single conversion
   * point and validates them, because the column `CHECK` admits
   * `absolute_indicator` — a value the app cannot map today.
   */
  amount_mode: AmountMode;
  sign_convention: SignConvention;
  /**
   * Normalized header labels seen at the last successful import, for drift
   * detection. NOT a format field: written by #330, not by the codec.
   */
  header_signature?: string | null;
  /**
   * Provenance tag only — records which template this source was configured
   * from, and is NEVER re-read as format. The eight format fields above are
   * authoritative, so editing a template alters no linked source.
   */
  template_id?: number | null;
  created_at: string;
  updated_at: string;
}

export interface ImportedFile {
  id: number;
  source_id: number;
  filename: string;
  file_hash: string;
  import_date: string;
  row_count: number;
  status: "completed" | "partial" | "error";
  notes?: string;
}

export interface ImportedFileWithSource extends ImportedFile {
  source_name: string;
}

export interface Category {
  id: number;
  name: string;
  parent_id?: number;
  color?: string;
  icon?: string;
  type: "expense" | "income" | "transfer";
  is_active: boolean;
  is_inputable: boolean;
  sort_order: number;
  /**
   * Translation key into `src/i18n/locales/*.json` under `categoriesSeed.*`.
   * Populated by the v1 IPC-aligned seed; NULL for user-created categories
   * and for v2 seed rows on existing profiles. Renderers must fall back to
   * `name` when absent.
   */
  i18n_key?: string | null;
  created_at: string;
}

export interface Supplier {
  id: number;
  name: string;
  normalized_name: string;
  category_id?: number;
  is_active: boolean;
  created_at: string;
  updated_at: string;
}

export interface Keyword {
  id: number;
  keyword: string;
  category_id: number;
  supplier_id?: number;
  priority: number;
  is_active: boolean;
}

export interface Transaction {
  id: number;
  date: string;
  description: string;
  amount: number;
  category_id?: number;
  supplier_id?: number;
  source_id?: number;
  file_id?: number;
  original_description?: string;
  notes?: string;
  is_manually_categorized: boolean;
  is_split: boolean;
  parent_transaction_id?: number;
  created_at: string;
  updated_at: string;
}

export interface Adjustment {
  id: number;
  name: string;
  description?: string;
  date: string;
  is_recurring: boolean;
  recurrence_rule?: string;
  created_at: string;
  updated_at: string;
}

export interface AdjustmentEntry {
  id: number;
  adjustment_id: number;
  category_id: number;
  amount: number;
  description?: string;
}

export interface BudgetEntry {
  id: number;
  category_id: number;
  year: number;
  month: number;
  amount: number;
  notes?: string;
  created_at: string;
  updated_at: string;
}

export interface BudgetTemplate {
  id: number;
  name: string;
  description?: string;
  created_at: string;
}

export interface BudgetTemplateEntry {
  id: number;
  template_id: number;
  category_id: number;
  amount: number;
}

export interface BudgetRow {
  category_id: number;
  category_name: string;
  category_color: string;
  category_type: "expense" | "income" | "transfer";
  planned: number;
  actual: number;
  difference: number;
  notes?: string;
}

export interface BudgetYearRow {
  category_id: number;
  category_name: string;
  category_color: string;
  category_type: "expense" | "income" | "transfer";
  parent_id: number | null;
  is_parent: boolean;
  depth?: number;
  months: number[]; // index 0-11 = Jan-Dec planned amounts
  annual: number;   // computed sum
  previousYearTotal: number; // actual (transactions) total from the previous year
}

/**
 * A named, reusable format. It carries exactly the eight persisted format
 * fields (via `ImportFormatRow`) plus its identity — the same eight an
 * `import_sources` row carries since v17.
 */
export interface ImportConfigTemplate extends ImportFormatRow {
  id: number;
  name: string;
  created_at: string;
}

export interface UserPreference {
  key: string;
  value: string;
  updated_at: string;
}

export interface NavItem {
  key: string;
  path: string;
  icon: string;
  labelKey: string;
  /**
   * Gated feature backing this nav entry. When set, the Sidebar shows a lock
   * icon if the license is not entitled (and ready) — the item stays clickable
   * (the gated route renders the upsell). Absent = never gated (Free module).
   */
  feature?: FeatureKey;
}

// --- Import Wizard Types ---

export interface ScannedFile {
  filename: string;
  file_path: string;
  size_bytes: number;
  modified_at: string;
}

export interface ScannedSource {
  folder_name: string;
  folder_path: string;
  files: ScannedFile[];
}

export interface ColumnMapping {
  date: number;
  description: number;
  amount?: number;
  debitAmount?: number;
  creditAmount?: number;
}

export type AmountMode = "single" | "debit_credit";
export type SignConvention = "negative_expense" | "positive_expense";

/**
 * --- The import format ------------------------------------------------------
 *
 * The eight fields that fully decide how a CSV file is read. They exist in two
 * shapes that CANNOT be a single composed type: the persisted rows are
 * snake_case with the mapping serialized to JSON and no boolean type in SQLite,
 * while the wizard works in camelCase on a parsed mapping.
 *
 * The guarantee that no field is ever half-persisted therefore does not come
 * from the type structure but from `src/utils/importFormat.ts`, the SINGLE
 * conversion point between the two shapes, and from its completeness test.
 */

/** Persisted shape — snake_case, mapping as JSON, `has_header` normalized to 0/1. */
export interface ImportFormatRow {
  delimiter: string;
  encoding: string;
  date_format: string;
  skip_lines: number;
  /** SQLite has no boolean: written as 0/1. Reads tolerate both, see `ImportFormatRowInput`. */
  has_header: number;
  column_mapping: string;
  amount_mode: AmountMode;
  sign_convention: SignConvention;
}

/**
 * What `formatFromRow` accepts. `import_sources` rows declare `has_header` as a
 * boolean and `import_config_templates` rows as a number; both are the same 0/1
 * integer at runtime, and the codec normalizes it. Widening the field here is
 * what lets a row from either table be decoded without a cast.
 */
export type ImportFormatRowInput = Omit<ImportFormatRow, "has_header"> & {
  has_header: number | boolean;
};

/** Domain shape — camelCase, mapping parsed. What the wizard manipulates. */
export interface ImportFormat {
  delimiter: string;
  encoding: string;
  dateFormat: string;
  skipLines: number;
  hasHeader: boolean;
  columnMapping: ColumnMapping;
  amountMode: AmountMode;
  signConvention: SignConvention;
}

/** A format plus the source it belongs to. */
export interface SourceConfig extends ImportFormat {
  name: string;
}

export interface ParsedRow {
  rowIndex: number;
  raw: string[];
  parsed: {
    date: string;
    description: string;
    amount: number;
  } | null;
  error?: string;
  sourceFilename?: string;
}

export interface DuplicateRow {
  rowIndex: number;
  date: string;
  description: string;
  amount: number;
  existingTransactionId: number;
}

export interface DuplicateCheckResult {
  fileAlreadyImported: boolean;
  existingFileId?: number;
  duplicateRows: DuplicateRow[];
  newRows: ParsedRow[];
}

export interface ImportReport {
  totalRows: number;
  importedCount: number;
  skippedDuplicates: number;
  errorCount: number;
  categorizedCount: number;
  uncategorizedCount: number;
  errors: Array<{ rowIndex: number; message: string }>;
}

// --- Dashboard Types ---

export type DashboardPeriod = "month" | "3months" | "6months" | "year" | "12months" | "all" | "custom";

export interface DashboardSummary {
  totalCount: number;
  totalAmount: number;
  incomeTotal: number;
  expenseTotal: number;
}

export interface CategoryBreakdownItem {
  category_id: number | null;
  category_name: string;
  category_color: string;
  total: number;
}

export interface RecentTransaction {
  id: number;
  date: string;
  description: string;
  amount: number;
  category_name: string | null;
  category_color: string | null;
}

// --- Report Types ---

export type ReportTab = "trends" | "byCategory" | "overTime" | "budgetVsActual";

/**
 * Shared filter state for the report pages: the active period plus the
 * account (import source) subset. `accountIds` is a list of `ImportSource.id`
 * — not to be confused with the Bilan module's `balance_accounts` ids, a
 * disjoint id space. An empty list means "no filter" (all accounts).
 */
export interface ReportFilters {
  period: DashboardPeriod;
  accountIds: number[];
}

export interface CategoryDelta {
  categoryId: number | null;
  categoryName: string;
  categoryColor: string;
  // Monthly block — single reference month vs its comparison month.
  // Old field names (previousAmount / currentAmount / deltaAbs / deltaPct) are
  // kept as aliases of the monthly values for backward compatibility with the
  // Highlights hub, Cartes top movers and the Compare chart, all of which show
  // monthly deltas only.
  previousAmount: number;
  currentAmount: number;
  deltaAbs: number;
  deltaPct: number | null; // null when previous is 0
  // Cumulative block — YTD window. For MoM, current = Jan→refMonth of refYear,
  // previous = Jan→(refMonth-1) of refYear (progress through last month).
  // For YoY, current = Jan→refMonth of refYear, previous = Jan→refMonth of
  // refYear-1. Values are populated only by the Compare service; other
  // consumers (Highlights, Cartes) leave them mirrored on the monthly block
  // since they do not expose a cumulative view.
  cumulativePreviousAmount: number;
  cumulativeCurrentAmount: number;
  cumulativeDeltaAbs: number;
  cumulativeDeltaPct: number | null;
  // Hierarchy block (Issue #247) — populated ONLY by the real-vs-real Compare
  // tree builder (getCompareMonthOverMonth / getCompareYearOverYear). Flat
  // consumers (Highlights movers, Cartes top movers) leave these undefined and
  // must not read them. Snake_case (unlike the camelCase value fields above) to
  // mirror BudgetVsActualRow and stay compatible with the shared `reorderRows`
  // util. `is_parent` rows are subtotals whose value = the net of their group.
  parent_id?: number | null;
  is_parent?: boolean;
  depth?: number;
  category_type?: "expense" | "income" | "transfer";
}

// Historical alias — used by the highlights hub. Shape identical to CategoryDelta.
export type HighlightMover = CategoryDelta;

export interface CategoryZoomChild {
  categoryId: number;
  categoryName: string;
  categoryColor: string;
  total: number;
}

export interface CategoryZoomEvolutionPoint {
  month: string;
  total: number;
}

export interface CategoryZoomData {
  rollupTotal: number;
  byChild: CategoryZoomChild[];
  monthlyEvolution: CategoryZoomEvolutionPoint[];
  transactions: RecentTransaction[];
}

export interface MonthBalance {
  month: string; // "YYYY-MM"
  netBalance: number;
}

export interface HighlightsData {
  currentMonth: string; // "YYYY-MM"
  netBalanceCurrent: number;
  netBalanceYtd: number;
  monthlyBalanceSeries: MonthBalance[]; // last 12 months ending at currentMonth
  topMovers: HighlightMover[];
  topTransactions: RecentTransaction[];
}

export interface MonthlyTrendItem {
  month: string;       // "2025-01"
  income: number;
  expenses: number;
}

export interface CategoryOverTimeItem {
  month: string;
  [categoryName: string]: number | string;
}

/**
 * One row of the id-keyed trends tree (Issue #264) — the sidecar to the
 * name-keyed pivot below. Carries a per-month magnitude series instead of a
 * single delta, but mirrors `CategoryDelta`'s snake_case hierarchy block
 * (`parent_id` / `is_parent` / `depth` / `category_type`) so it composes with
 * `collapsibleRows` / `useCollapsibleGroups` without friction. Section/type is
 * resolved by `category_id` (never by name), so two homonym categories of
 * different types never collide — the fix the name-keyed pivot cannot express.
 */
export interface OverTimeRow {
  categoryId: number | null;
  categoryName: string;
  categoryColor: string;
  /** Positive magnitude per month, index-aligned to `CategoryOverTimeData.data`. */
  monthly: number[];
  /** Sum of `monthly` — sibling ordering + row total convenience. */
  total: number;
  // Hierarchy block — same snake_case names as CategoryDelta (Issue #247/#264).
  // `is_parent` rows are subtotals whose series = the sum of their leaf subtree.
  parent_id: number | null;
  is_parent: boolean;
  depth: number;
  category_type: "expense" | "income" | "transfer";
}

export interface CategoryOverTimeData {
  categories: string[];
  data: CategoryOverTimeItem[];
  colors: Record<string, string>;
  categoryIds: Record<string, number | null>;
  /** Category name -> its type. Built from the top-N rows (mirrors `colors`). */
  types: Record<string, "expense" | "income" | "transfer">;
  /**
   * Id-keyed hierarchical sidecar (Issue #264): EVERY category (no top-N, no
   * "Other" bucket), parent-first depth-first, income → expense → transfer.
   * Drives the trends table + its result lines. The pivot fields above are left
   * unchanged and still drive the (top-N-capped) chart and the dashboard.
   */
  tree: OverTimeRow[];
}

export interface BudgetVsActualRow {
  category_id: number;
  category_name: string;
  category_color: string;
  category_type: "expense" | "income" | "transfer";
  parent_id: number | null;
  is_parent: boolean;
  depth?: number;
  monthActual: number;
  monthBudget: number;
  monthVariation: number;
  monthVariationPct: number | null;
  ytdActual: number;
  ytdBudget: number;
  ytdVariation: number;
  ytdVariationPct: number | null;
}

// --- Cartes (Issue #97) — dashboard snapshot ---

export interface CartesSparklinePoint {
  month: string; // "YYYY-MM"
  value: number;
}

export interface CartesKpi {
  // `current` is nullable for ratio-style KPIs (e.g. savings rate) when the
  // denominator is zero and the value is genuinely undefined rather than 0.
  current: number | null;
  previousMonth: number | null;
  previousYear: number | null;
  deltaMoMAbs: number | null;
  deltaMoMPct: number | null;
  deltaYoYAbs: number | null;
  deltaYoYPct: number | null;
  sparkline: CartesSparklinePoint[]; // 13 months ending at reference month
}

export type CartesKpiId = "income" | "expenses" | "net" | "savingsRate";

/**
 * Period mode for the Cartes KPI cards.
 *   - "month": KPI value = reference month only.
 *   - "ytd":   KPI value = cumulative from Jan 1st of the reference year up to
 *              the end of the reference month. The sparkline is not affected,
 *              only the rendered "current" value and its MoM/YoY deltas.
 */
export type CartesKpiPeriodMode = "month" | "ytd";

export interface CartesTopMover {
  categoryId: number | null;
  categoryName: string;
  categoryColor: string;
  previousAmount: number;
  currentAmount: number;
  deltaAbs: number;
  deltaPct: number | null;
}

export interface CartesMonthFlow {
  month: string; // "YYYY-MM"
  income: number;
  expenses: number;
  net: number;
}

export interface CartesBudgetWorstOverrun {
  categoryId: number;
  categoryName: string;
  categoryColor: string;
  budget: number;
  actual: number;
  overrunAbs: number;
  overrunPct: number | null;
}

export interface CartesBudgetAdherence {
  categoriesInTarget: number;
  categoriesTotal: number;
  worstOverruns: CartesBudgetWorstOverrun[];
}

export interface CartesSeasonalityYear {
  year: number;
  amount: number;
}

export interface CartesSeasonality {
  referenceAmount: number;
  historicalYears: CartesSeasonalityYear[]; // up to 2 previous years
  historicalAverage: number | null;
  deviationPct: number | null;
}

export interface CartesSnapshot {
  referenceYear: number;
  referenceMonth: number;
  kpis: {
    income: CartesKpi;
    expenses: CartesKpi;
    net: CartesKpi;
    savingsRate: CartesKpi; // value stored as 0-100
  };
  flow12Months: CartesMonthFlow[];
  topMoversUp: CartesTopMover[];
  topMoversDown: CartesTopMover[];
  budgetAdherence: CartesBudgetAdherence;
  seasonality: CartesSeasonality;
}

export type ImportWizardStep =
  | "source-list"
  | "source-config"
  | "file-preview"
  | "duplicate-check"
  | "confirm"
  | "importing"
  | "report";

// --- Category Page Types ---

export interface CategoryTreeNode {
  id: number;
  name: string;
  parent_id: number | null;
  color: string | null;
  icon: string | null;
  type: "expense" | "income" | "transfer";
  is_active: boolean;
  is_inputable: boolean;
  sort_order: number;
  keyword_count: number;
  i18n_key: string | null;
  children: CategoryTreeNode[];
}

export interface CategoryFormData {
  name: string;
  type: "expense" | "income" | "transfer";
  color: string;
  parent_id: number | null;
  is_inputable: boolean;
  sort_order: number;
}

// --- Transaction Page Types ---

export interface TransactionRow {
  id: number;
  date: string;
  description: string;
  amount: number;
  category_id: number | null;
  category_name: string | null;
  category_color: string | null;
  source_name: string | null;
  notes: string | null;
  is_manually_categorized: boolean;
  is_split: boolean;
}

export interface SplitChild {
  id: number;
  category_id: number | null;
  category_name: string | null;
  category_color: string | null;
  amount: number;
  description: string;
}

export interface TransactionFilters {
  search: string;
  categoryId: number | null;
  sourceId: number | null;
  dateFrom: string | null;
  dateTo: string | null;
  uncategorizedOnly: boolean;
}

export interface TransactionSort {
  column: "date" | "description" | "amount" | "category_name";
  direction: "asc" | "desc";
}

export interface TransactionPageResult {
  rows: TransactionRow[];
  totalCount: number;
  totalAmount: number;
  incomeTotal: number;
  expenseTotal: number;
}

// --- Balance (Bilan) types ---
// Backed by migration v9 (see src-tauri/src/database/balance_schema.sql).
// MVP scope (Issue #138 / #1a): categories + accounts CRUD only. Snapshots,
// snapshot lines and transfers ship in subsequent issues (#1b / #2 / #4).

export type BalanceCategoryKind = "simple" | "priced";

/**
 * Account entry mode (Bilan détail par titre, migration v15). 'simple' = the
 * snapshot line stores one denormalized value; 'detailed' = the line is broken
 * down into per-security holdings (see BalanceSnapshotHolding). Accounts under a
 * priced category are stamped 'detailed' by the v15 backfill. Distinct from
 * BalanceCategoryKind: a category is simple/priced, an account is
 * simple/detailed — the pivot is per-account, not per-category.
 */
export type BalanceAccountKind = "simple" | "detailed";

/**
 * Asset class for priced categories. Required when `kind === 'priced'` so
 * PriceFetchControl can route to the right provider (best-effort Yahoo for
 * stocks, exchange APIs for crypto). NULL for simple kind, and for legacy
 * priced rows created before migration v10 — those are read-only for the
 * fetch flow until the user edits the category.
 */
export type BalanceAssetType = "stock" | "crypto";

/**
 * Fiscal envelope / tax shelter attached to an account (Bilan axe véhicule,
 * migration v12). This is the registration status of the account, NOT an
 * automobile type. NULL/absent means no envelope (e.g. a chequing account or
 * a crypto wallet). The asset class lives separately on the category.
 *   unregistered = Non-enregistré · tfsa = CELI · rrsp = REER · rrif = FERR
 *   fhsa = CELIAPP · resp = REEE
 */
export type BalanceVehicleType =
  | "unregistered"
  | "tfsa"
  | "rrsp"
  | "rrif"
  | "fhsa"
  | "resp";

/**
 * The six recognized fiscal envelopes, in display order. Single source of
 * truth shared by the account form dropdown (Issue #203) and the service-side
 * `normalizeVehicleType` validation. Kept in sync with the SQL CHECK on
 * `balance_accounts.vehicle_type`.
 */
export const BALANCE_VEHICLE_TYPES: readonly BalanceVehicleType[] = [
  "unregistered",
  "tfsa",
  "rrsp",
  "rrif",
  "fhsa",
  "resp",
];

export const BALANCE_CURRENCY_CAD = "CAD";

export interface BalanceCategory {
  id: number;
  /** Stable lookup key (e.g. 'cash', 'tfsa', 'stock'). UNIQUE NOT NULL. */
  key: string;
  /** Translation key into i18n locales (e.g. 'balance.category.cash'). */
  i18n_key: string;
  /** simple = direct value entry; priced = quantity x unit_price. */
  kind: BalanceCategoryKind;
  sort_order: number;
  is_active: boolean;
  /** True when seeded by Migration v9 — cannot be deleted, can be renamed. */
  is_seed: boolean;
  /**
   * Asset class for priced kind ('stock' | 'crypto'). NULL for simple kind
   * and for legacy priced rows created before migration v10. The category
   * form requires it on creation when kind=priced.
   */
  asset_type: BalanceAssetType | null;
  /**
   * User-facing label override (migration v12). When set (non-empty), the UI
   * renders this verbatim; otherwise it falls back to `t(i18n_key)`. This is
   * how category renaming works now — it never touches `i18n_key` anymore
   * (fixes bug I where renaming clobbered the translation key).
   */
  custom_label?: string | null;
}

export interface BalanceAccount {
  id: number;
  balance_category_id: number;
  name: string;
  /** Symbol (e.g. 'AAPL', 'BTC-USD'); NULL for simple-kind accounts. */
  symbol: string | null;
  /** ISO 4217. MVP: hardcoded 'CAD' (CHECK enforced server-side). */
  currency: string;
  notes: string | null;
  is_active: boolean;
  /** Soft-delete timestamp; archived accounts hide from new snapshots. */
  archived_at: string | null;
  /**
   * Fiscal envelope / tax shelter (migration v12). NULL = no envelope. NOT an
   * automobile type. The asset class lives on the linked category.
   */
  vehicle_type?: BalanceVehicleType | null;
  /**
   * Entry mode (migration v15). 'simple' = one denormalized value per snapshot
   * line; 'detailed' = a basket of per-security holdings. Defaults to 'simple';
   * priced-category accounts are backfilled to 'detailed'.
   */
  kind: BalanceAccountKind;
  /**
   * Authoritative pivot date (ISO YYYY-MM-DD) from which detailed entry is
   * expected for this account (migration v15). NULL until the conversion flow
   * sets it; snapshots before this date stay simple.
   */
  detailed_since?: string | null;
  created_at: string;
  updated_at: string;
}

/** Joined view used by AccountsPage tables. */
export interface BalanceAccountWithCategory extends BalanceAccount {
  category_key: string;
  category_i18n_key: string;
  category_kind: BalanceCategoryKind;
  /** Mirror of `balance_categories.asset_type` — drives PriceFetchControl. */
  category_asset_type: BalanceAssetType | null;
  /** Mirror of `balance_categories.custom_label` — drives renderCategoryLabel. */
  category_custom_label?: string | null;
  // Note: the account's own `kind` (simple|detailed, from BalanceAccount) is
  // distinct from `category_kind` (simple|priced) — both are exposed here.
}

// Détail par titre (Bilan Étape 2) — migrations v14/v15. A detailed account's
// snapshot line is broken down into per-security holdings. Securities are a
// shared catalogue keyed by normalized symbol; holdings reference the snapshot
// line (one line per snapshot+account) plus a security.

/**
 * An investable instrument (stock or crypto) held in a detailed account.
 * Backed by `balance_securities` (migration v14). `symbol` is stored normalized
 * (upper/trim) and is UNIQUE COLLATE NOCASE so case-variants can't duplicate.
 */
export interface BalanceSecurity {
  id: number;
  /** Normalized (upper/trim) symbol, e.g. 'AAPL', 'BTC'. UNIQUE NOCASE. */
  symbol: string;
  /** Human-readable name (e.g. 'Apple Inc.'); optional. */
  name: string | null;
  /** ISO 4217. Defaults to 'CAD'. */
  currency: string;
  /** Routes the price-fetch flow (stock vs crypto provider). */
  asset_type: BalanceAssetType;
  created_at: string;
  updated_at: string;
}

/**
 * One per-security line within a detailed account's snapshot line. Backed by
 * `balance_snapshot_holdings` (migration v14). `value` is denormalized
 * (= quantity * unit_price) for reproducible reports. `book_cost` (optional)
 * feeds the unrealized-gain column.
 */
export interface BalanceSnapshotHolding {
  id: number;
  /** FK to balance_snapshot_lines(id) — the (snapshot, account) pair. */
  snapshot_line_id: number;
  security_id: number;
  quantity: number;
  unit_price: number;
  value: number;
  /** Acquisition cost basis for the unrealized-gain column; optional. */
  book_cost: number | null;
  /** 'manual' | 'maximus-api' | NULL. */
  price_source: string | null;
  price_fetched_at: string | null;
  created_at: string;
  updated_at: string;
}

/**
 * Joined view exposing the security's symbol/asset_type alongside the holding —
 * used by the per-title drill-down table so it can render and route prices
 * without a second lookup.
 */
export interface BalanceSnapshotHoldingWithSecurity
  extends BalanceSnapshotHolding {
  security_symbol: string;
  security_name: string | null;
  security_asset_type: BalanceAssetType;
}

// Snapshots — added Issue #146 (Bilan #1b) for the SnapshotEditPage.
// Lines are kept simple-kind only here (`quantity` / `unit_price` always NULL).
// The priced-kind UI lands in #140 / Bilan #2.

export interface BalanceSnapshot {
  id: number;
  /** ISO date (YYYY-MM-DD), UNIQUE across the table. */
  snapshot_date: string;
  notes: string | null;
  created_at: string;
  updated_at: string;
}

export interface BalanceSnapshotLine {
  id: number;
  snapshot_id: number;
  account_id: number;
  /** Always NULL for simple-kind lines (Issue #146 scope). */
  quantity: number | null;
  /** Always NULL for simple-kind lines (Issue #146 scope). */
  unit_price: number | null;
  value: number;
  /** 'manual' for simple-kind, 'maximus-api' for priced (#142). */
  price_source: string | null;
  price_fetched_at: string | null;
  created_at: string;
  updated_at: string;
}

// Account transfers — added Issue #142 (Bilan #4). Links a transaction to a
// balance account so the Modified Dietz return calculator can separate
// contributions from gains. Direction follows the account's perspective:
//   'in'  = capital added (deposit / buy)
//   'out' = capital removed (withdrawal / sell)
// `transaction_id` ON DELETE RESTRICT — preserves reproducibility of past
// returns, the UI must force the user to unlink before deleting the
// underlying transaction.
export type BalanceTransferDirection = "in" | "out";

export interface BalanceAccountTransfer {
  id: number;
  account_id: number;
  transaction_id: number;
  direction: BalanceTransferDirection;
  notes: string | null;
  created_at: string;
}

/** Joined view used by LinkTransfersModal + transaction icon lookup. */
export interface BalanceAccountTransferWithTransaction
  extends BalanceAccountTransfer {
  transaction_date: string;
  transaction_description: string;
  transaction_amount: number;
  account_name: string;
}

/**
 * Modified Dietz return for one account over a period.
 * Mirrors the Rust struct in `src-tauri/src/commands/return_calculator.rs`.
 *
 *   - `value_start` / `value_end`: latest snapshot value ≤ each endpoint, or
 *     null when no snapshot exists.
 *   - `net_contributions`: signed sum of cash flows in the period.
 *   - `return_pct`: Modified Dietz return (0.05 = +5%); null if either
 *     endpoint is missing or denominator is non-positive.
 *   - `annualized_pct`: `(1 + R)^(365/T) - 1`; null for zero-length periods
 *     or whenever `return_pct` is null.
 *   - `is_partial`: true when one endpoint snapshot is missing.
 *   - `has_no_transfers_warning`: true when no transfers were tagged in the
 *     period — return collapses to simple `(V_end - V_start) / V_start` and
 *     the UI surfaces a warning so the user can verify nothing was forgotten.
 */
export interface AccountReturn {
  value_start: number | null;
  value_end: number | null;
  net_contributions: number;
  return_pct: number | null;
  annualized_pct: number | null;
  is_partial: boolean;
  has_no_transfers_warning: boolean;
}
