// Trade-In Estimator — TypeScript types
// Mirrors Pydantic schemas from backend/app/schemas/trade_in.py.

// ── Quota ──
export interface QuotaStatus {
  limit: number;
  used: number;
  remaining: number;
  unlimited: boolean;
}

export type HouseType =
  | "panel"
  | "brick"
  | "monolith"
  | "monolith_brick"
  | "other";

export type RepairState = "needs_repair" | "standard" | "good" | "excellent";

// Runtime-валидаторы enum-значений (#770 finding #30): защищают `as`-cast от
// произвольной строки (битый ?id=-restore / подменённый payload).
export const HOUSE_TYPES: readonly HouseType[] = [
  "panel",
  "brick",
  "monolith",
  "monolith_brick",
  "other",
];
export const REPAIR_STATES: readonly RepairState[] = [
  "needs_repair",
  "standard",
  "good",
  "excellent",
];
export function asHouseType(v: string | null | undefined): HouseType | undefined {
  return v && (HOUSE_TYPES as readonly string[]).includes(v) ? (v as HouseType) : undefined;
}
export function asRepairState(v: string | null | undefined): RepairState | undefined {
  return v && (REPAIR_STATES as readonly string[]).includes(v) ? (v as RepairState) : undefined;
}

export type ConfidenceLevel = "low" | "medium" | "high";

// Точность гео-привязки адреса (из DaData qc_geo): house=0, street=1, approximate≥2.
export type AddressPrecision = "house" | "street" | "approximate";

// Структурный уровень аналогов (#695 follow-up) — бэкенд отдаёт начиная с deploy ветки
// feat/estimator-ui-transparency. До деплоя поле absent/null → graceful fallback на compsTier().
export type AnalogTier = "same_building" | "micro_radius" | "district" | "city";

export interface TradeInEstimateInput {
  address: string; // min 3, max 500
  area_m2: number; // 10 < x < 500
  rooms: number; // 0-10 (0 = студия)
  floor: number | null; // 1-100, optional
  total_floors: number | null; // 1-100, optional
  year_built?: number; // 1800-2100
  house_type?: HouseType;
  repair_state?: RepairState;
  has_balcony?: boolean;
  // CRM-поля (#395)
  ownership_type?: string;
  has_mortgage?: boolean;
  // client_name / client_phone удалены (PII purge #1969, DROP COLUMN 167).
  // Координаты из автокомплита — позволяют бэкенду пропустить геокодинг
  // для адресов, которые DaData/Yandex не резолвят по строке.
  lat?: number | null;
  lon?: number | null;
  // Радиус поиска аналогов/сделок в метрах (опциональный override дефолта бэкенда).
  // Зеркалит новое поле request-схемы; absent → бэкенд применяет свой дефолт.
  radius_m?: number | null;
  // ФИАС-идентификатор дома из выбранной подсказки автокомплита (house-level,
  // DaData-тир). Детерминированный ключ дома — бэкенд использует его ПЕРВЫМ,
  // до геокодинга строки. Отправляется ТОЛЬКО когда адрес выбран из подсказки;
  // сбрасывается в null при ручной правке адреса или сдвиге пина на карте.
  target_fias_id?: string | null;
}

export interface AnalogLot {
  address: string;
  area_m2: number;
  rooms: number;
  floor: number | null;
  total_floors: number | null;
  price_rub: number;
  price_per_m2: number;
  listing_date: string | null; // ISO date
  days_on_market: number | null;
  photo_url: string | null;
  // ── Слой 5.2 — clickable links + distance ──
  source: string | null;       // 'avito' / 'cian' / 'domklik' / 'rosreestr'
  source_url: string | null;   // ссылка на оригинал
  distance_m: number | null;   // расстояние до целевой квартиры в метрах
  // ── PR M (#564 Phase 3) — confidence tier для rosreestr deals ──
  // 'T0_per_house' — kadastr_num exact match (currently not available in open dataset)
  // 'T1_per_street' — street-level only (default for all rosreestr deals)
  tier?: string | null;
  // ── ANALYTICS surface — гео-координаты лота для карты (MapCard). Optional +
  //    nullable: старые оценки и лоты без геокодинга их не содержат. Пропускаем
  //    пины с null lat/lon.
  lat?: number | null;
  lon?: number | null;
}

// ── ANALYTICS surface — точка тренда ₽/м² по месяцам (PriceTrendCard) ──
// month — "YYYY-MM"; ppm2 — медиана ₽/м² за месяц по зданию/району.
export interface PriceTrendPoint {
  month: string; // "YYYY-MM"
  ppm2: number;  // ₽/м²
}

export interface CianValuationSummary {
  sale_price_rub: number | null;
  rent_price_rub: number | null;
  chart: Array<{ date: string; price: number }>;
  chart_change_pct: number | null;
  chart_change_direction: 'increase' | 'decrease' | 'neutral' | null;
}

// Базис коэффициента asking→sold (S3): по группе комнат либо городской fallback.
export type RatioBasis = "per_rooms" | "global_fallback";

// ── #651: Avito IMV (Индекс Market Value) якорь для target-дома ──
// Реальная рыночная оценка Avito по дому (house_imv_evaluations) — anchor для
// blend'а медианы. Сурфейсится как референсный маркер. null при отсутствии записи.
export interface AvitoImvSummary {
  recommended_price: number | null; // рекомендованная цена Avito, ₽
  lower_price: number | null;       // нижняя граница IMV-коридора, ₽
  higher_price: number | null;      // верхняя граница IMV-коридора, ₽
  market_count: number | null;      // объём рынка, на котором построена оценка
}

// ── #652: коридор реальных ДКП-сделок Росреестра (advisory, не клампит) ──
// Агрегаты ₽/м² по сопоставимым сделкам за период. null / count=0 если по улице
// нет сделок. count..period_months — required (бэкенд не отдаёт частичный объект).
export interface DkpCorridor {
  count: number;          // число ДКП-сделок в выборке
  low_ppm2: number;       // P10 ₽/м² по сделкам (робастный коридор)
  median_ppm2: number;    // медиана ₽/м²
  high_ppm2: number;      // P90 ₽/м² по сделкам (робастный коридор)
  period_months: number;  // окно поиска сделок
}

export interface AggregatedEstimate {
  estimate_id: string; // UUID
  median_price_rub: number; // ASKING-ориентир: по чему выставлены аналоги
  range_low_rub: number;
  range_high_rub: number;
  median_price_per_m2: number;
  // ── Ожидаемая цена продажи (S3) — реалистичная цена сделки по ДКП Росреестра,
  //    обычно ~на 15–20% ниже asking. Все поля optional + nullable: старые оценки
  //    их не содержат, а graceful-degradation отдаёт null при пустой ratio-таблице.
  expected_sold_price_rub?: number | null;
  expected_sold_range_low_rub?: number | null;
  expected_sold_range_high_rub?: number | null;
  expected_sold_per_m2?: number | null;
  asking_to_sold_ratio?: number | null; // sold / asking (≈0.82)
  ratio_basis?: RatioBasis | null;       // 'per_rooms' | 'global_fallback'
  confidence: ConfidenceLevel;
  confidence_explanation: string | null;
  n_analogs: number;
  insufficient_data: boolean; // backend #697: true когда median_price_rub <= 0 (нет данных)
  period_months: number; // 24
  analogs: AnalogLot[]; // top 5-10
  actual_deals: AnalogLot[]; // last 12 mo
  expires_at: string; // ISO datetime
  // ── Метаданные ──
  target_address: string | null;
  target_lat: number | null;
  target_lon: number | null;
  sources_used: string[];                // ['avito', 'cian', 'rosreestr']
  // #2043 (BE-1): достоверность выборки — реальный коэффициент вариации ₽/м² (std/mean),
  // счётчики аналогов по источнику, момент создания оценки. Все optional: старые
  // кешированные оценки их не содержат → UI graceful fallback.
  cv?: number | null;                      // коэффициент вариации ₽/м² (0..1), null если <2 цен
  source_counts?: Record<string, number>;  // {'avito': 12, 'cian': 5}
  created_at?: string | null;              // ISO datetime — «отчёт от DD.MM»
  data_freshness_minutes: number | null; // «обновлено N минут назад»
  last_scraped_at?: string | null;       // ISO datetime последнего скрейпа источников (optional)
  est_days_on_market: number | null;     // прогноз срока продажи
  // address_precision — точность гео-привязки адреса (из DaData qc_geo):
  // «house» (qc_geo=0, дом точно), «street» (qc_geo=1, до улицы),
  // «approximate» (qc_geo≥2: населённый пункт/город/регион/не распознан).
  // null если DaData не отрабатывала (адрес не геокодирован) / credentials не заданы.
  address_precision?: AddressPrecision | null;
  // ── Структурный уровень аналогов (feat/estimator-ui-transparency) ──
  // Появляется после деплоя backend'а; до этого absent/null → UI graceful fallback.
  analog_tier?: AnalogTier | null;
  // ── Параметры оценённой квартиры (для восстановления карточки по ?id=) ──
  area_m2: number | null;
  rooms: number | null;
  floor: number | null;
  total_floors: number | null;
  year_built: number | null;
  house_type: HouseType | null;
  repair_state: RepairState | null;
  has_balcony: boolean | null;
  // ── Cian valuation surface ──
  cian_valuation?: CianValuationSummary | null;
  // ── #651/#652: внешние якоря ──
  // avito_imv    — реальная Avito IMV-оценка дома, anchor для blend'а медианы.
  // dkp_corridor — коридор ₽/м² по ДКП-сделкам Росреестра (advisory). Оба nullable:
  //                null при отсутствии данных (graceful, common case без регрессий).
  avito_imv?: AvitoImvSummary | null;
  dkp_corridor?: DkpCorridor | null;
  // ── ANALYTICS surface (web-native cards) ──
  // price_trend — динамика медианы ₽/м² по месяцам для здания/района
  //               (PriceTrendCard). null / <2 точек → карточка не рендерится.
  //               last_scraped_at объявлен выше (был дубль #688+#689 → ломал build).
  price_trend?: PriceTrendPoint[] | null;
  // ── #2002: сигналы «ручной оценки» и «премиального дома» ──
  // manual_review_recommended — авто-оценка рискованна, рекомендуется ручная оценка
  //   человеком (не презентовать как твёрдую оферту). manual_review_reasons — RU
  //   human-readable причины («премиальный дом …», «высокая стоимость», «низкая
  //   уверенность оценки», «широкий диапазон цены»). Оба optional: старые/кешированные
  //   оценки до деплоя бэкенда их не содержат → graceful (undefined ≈ false / []).
  manual_review_recommended?: boolean;
  manual_review_reasons?: string[];
  // premium_building — целевой объект в курируемом/известном премиальном доме.
  // premium_building_class — тариф класса когда известен: «премиум» | «элит» |
  //   «бизнес» (null если дом премиальный, но класс не определён).
  // premium_building_median_ppm2 — медиана ₽/м² по премиальному дому (existing,
  //   справочный якорь). null при отсутствии данных.
  premium_building?: boolean;
  premium_building_class?: string | null;
  premium_building_median_ppm2?: number | null;
}

// ── Stage 4a/4b response types ──

export interface HouseInfoForEstimate {
  house_id: number | null;
  ext_house_id: string | null;
  address: string | null;
  short_address: string | null;
  lat: number | null;
  lon: number | null;
  year_built: number | null;
  total_floors: number | null;
  house_type: string | null;
  passenger_elevators: number | null;
  cargo_elevators: number | null;
  has_concierge: boolean | null;
  closed_yard: boolean | null;
  has_playground: boolean | null;
  parking_type: string | null;
  developer_name: string | null;
  rating: number | null;
  reviews_count: number | null;
  raw_characteristics: Array<Record<string, unknown>>;
}

export interface PlacementHistoryItem {
  id: number;
  source: string;
  house_id: number;
  ext_item_id: string;
  title: string | null;
  rooms: number | null;
  area_m2: number | null;
  floor: number | null;
  total_floors: number | null;
  start_price: number | null;
  start_price_date: string | null;
  last_price: number | null;
  last_price_date: string | null;
  removed_date: string | null;
  exposure_days: number | null;
  notes: string | null;
}

export interface IMVBenchmarkResponse {
  available: boolean;
  cache_key: string | null;
  recommended_price: number | null;
  lower_price: number | null;
  higher_price: number | null;
  market_count: number | null;
  fetched_at: string | null;
  our_median_price: number | null;
  diff_pct: number | null;
}

// ── House Analytics (endpoint: GET /estimate/{id}/house-analytics) ──

export interface PriceHistoryYearPoint {
  year: number;
  source: string; // 'avito_imv' | 'yandex_valuation'
  median_price_per_m2: number;
  n_lots: number;
  median_price_rub: number;
}

export interface CianPriceChangeStats {
  cian_id: string;
  listing_id: number;
  n_changes: number;
  last_change_time: string | null;
  last_diff_percent: number | null;
  total_change_pct: number | null;
  first_seen_price: number | null;
  current_price: number;
}

export interface RecentSoldEntry {
  id: number;
  source: string; // 'avito_imv' | 'yandex_valuation'
  rooms: number | null;
  area_m2: number | null;
  floor: number | null;
  start_price: number | null;
  last_price: number | null;
  removed_date: string | null; // ISO date
  exposure_days: number | null;
  discount_pct: number | null; // (start - last) / start * 100
}

export interface HouseAnalyticsKpi {
  total_lots: number;
  sold_count: number;
  sold_rate_pct: number;
  median_exposure_days: number | null;
  median_bargain_pct: number | null;
}

export interface HouseAnalyticsResponse {
  house_ids: number[];
  radius_m: number; // 0 = exact house, 300 = соседи в радиусе
  price_history: PriceHistoryYearPoint[];
  recent_sold: RecentSoldEntry[];
  kpi: HouseAnalyticsKpi;
}

// ── Street deals (endpoint: GET /trade-in/street-deals) ──

export interface StreetDealsResponse {
  street: string | null;          // "Космонавтов" / null если не извлёкся
  period_from: string;            // ISO date "2025-05-29"
  period_to: string;
  count: number;                  // all matching, не топ-10
  median_price_rub: number;
  median_price_per_m2: number;
  range_low_rub: number;
  range_high_rub: number;
  deals: AnalogLot[];             // top-10 по deal_date DESC
}

// ── Sales vs Listings (endpoint: GET /trade-in/sales-vs-listings) ──
// Phase 2 PR L (#564): для каждой ДКП-сделки возвращает paired listing (если есть).
// LEFT JOIN — все deals сохраняются; listing_* = null при отсутствии match.

export interface SalesListingPair {
  deal_id: number;
  deal_date: string;              // ISO date
  deal_price_rub: number;
  deal_price_per_m2: number;
  deal_area_m2: number;
  deal_rooms: number;
  deal_floor: number | null;
  deal_address: string;

  listing_id: number | null;
  listing_source: string | null;       // 'avito' / 'cian' / 'yandex' / 'domklik' (истор. могут встречаться выключенные источники)
  listing_source_url: string | null;
  listing_date: string | null;         // ISO date
  listing_price_rub: number | null;
  listing_price_per_m2: number | null;
  listing_area_m2: number | null;

  // Положительный = listing date раньше сделки (типичный кейс).
  // Отрицательный = listing появился позже сделки (отложенный парсинг).
  days_listing_to_deal: number | null;
  // discount_pct = (deal_price - listing_price) / listing_price * 100.
  // Отрицательный = продали дешевле выставленного (нормальный торг).
  discount_pct: number | null;
}

export interface SalesVsListingsResponse {
  street: string | null;
  period_months: number;          // 24 default
  window_days: number;            // 180 default
  area_tolerance: number;         // 0.15 default
  total_deals: number;
  deals_with_listings: number;
  linkage_rate_pct: number;
  median_discount_pct: number | null;
  // Качество данных: house_linked = есть пары ДКП↔listing; street_only = есть
  // сделки, но привязка к конкретному дому/объявлению невозможна; no_data = нет сделок.
  data_quality: "house_linked" | "street_only" | "no_data";
  pairs: SalesListingPair[];      // все sorted by deal_date DESC
}

// ── Sell-time sensitivity (endpoint: GET /estimate/{id}/sell-time-sensitivity) ──

export interface SellTimeBucket {
  price_premium_label: string; // 'cheap' | 'median' | 'plus5' | 'plus10'
  price_premium_pct: number;   // -5, 0, 5, 10
  median_exposure_days: number | null;
  p25_days: number | null;
  p75_days: number | null;
  n_lots: number;
  insufficient_data?: boolean; // backend flags a bucket too thin to trust
}

export interface SellTimeSensitivityResponse {
  house_ids: number[];
  radius_m: number;
  target_median_price_per_m2: number | null;
  buckets: SellTimeBucket[]; // ровно 4 элемента: cheap, median, plus5, plus10
}

// ── Estimate history (endpoint: GET /trade-in/history?limit=N) ──
// Per-user список последних оценок (скоупится по X-Authenticated-User в бэке,
// #656). Лёгкая проекция строки trade_in_estimates — НЕ полный AggregatedEstimate.
// NB: area_m2 — NUMERIC колонка, FastAPI сериализует Decimal в СТРОКУ ("35.00").
// sources_used в этом эндпоинте отсутствует (см. mapCache TODO BE-1).
export interface EstimateHistoryItem {
  id: string; // UUID оценки (для restore по ?id=)
  address: string | null;
  rooms: number | null;
  area_m2: string | null; // Decimal -> строка, напр. "35.00"
  median_price: number;
  confidence: ConfidenceLevel;
  n_analogs: number;
  created_at: string; // ISO datetime
  // #2420 — вернулись в GET /history наравне с остальными параметрами оценки.
  floor: number | null;
  total_floors: number | null;
  year_built: number | null;
  house_type: string | null;
  repair_state: string | null;
  has_balcony: boolean | null;
}

// ── Geocode suggest (endpoint: GET /geocode/suggest?q=&limit=) ──
// Автокомплит адресов в пределах ЕКБ. Зеркалит SuggestItem / SuggestResponse
// из backend/app/api/v1/geocode.py.
export interface GeocodeSuggestion {
  label: string; // короткая подпись для дропдауна
  full_address: string; // полный адрес — то что подставляем в поле
  lat: number;
  lon: number;
  kind: string; // тип объекта (house / street / …)
  // ФИАС-идентификатор дома (заполнен только для house-level кандидатов
  // DaData-тира; для street/locality и не-DaData-источников — null/absent).
  fias_id?: string | null;
}

export interface GeocodeSuggestResponse {
  items: GeocodeSuggestion[];
}

// ── Trade-in lead capture (endpoint: POST /trade-in/lead) ──
// Issue #2377 (sub-issue родителя #1971) — заменяет mailto-стаб в HeroTransparency
// на реальную заявку. Зеркалит TradeInLeadInput из
// tradein-mvp/backend/app/api/v1/lead.py (issue #2376 / PR #2390).
// consent: Literal[True] на бэкенде — 422 при consent !== true, поэтому тип
// здесь тоже сужен до `true` (не `boolean`), чтобы TS не давал собрать невалидный payload.
export interface TradeInLeadInput {
  phone: string; // 5-32 символа, паттерн см. _PHONE_PATTERN в lead.py
  estimate_id?: string | null; // UUID; 404 если не найден
  consent: true;
  source?: "result" | "landing"; // default 'result' на бэкенде
}

export interface TradeInLeadResponse {
  id: string; // UUID лида
  created_at: string; // ISO datetime
  status: string; // 'received'
}

// ── Location index (endpoint: GET /trade-in/location-index?estimate_id=&radius_m=) ──
// Replaces the broken location-coef (#2045 audit — see
// backend/app/services/location_index.py for the full history: the old
// `coef = 0.95 + score/100*0.10` was clamped to ±5%, uncorrelated with real
// prices, and never actually fed the estimate). location_index_pct is the %
// deviation of the local median ₽/м² (comparable active listings near the
// address) from the citywide median ₽/м² — a real, uncalibrated-range
// comparison metric, NOT a price multiplier. It does NOT feed the estimate
// (analogs already carry location in the base price; folding this in again
// would double-count the same effect).
//
// status:
//   "ok"                — location_index_pct/local_median_price_per_m2 reliable.
//   "out_of_coverage"   — address outside the product's geo coverage
//     (Yekaterinburg only). All numeric fields null — an honest dash, not 0%.
//   "insufficient_data" — even at the widest search radius there are too few
//     comparable active listings. Numeric fields null; sample_size/radius_m
//     still report what was actually found.
//
// poi_status is independent of status above ("что рядом" and the numeric
// index degrade separately): "ok" | "unavailable" (local OSM POI mirror
// empty/not yet refreshed on this environment — never fabricated points).
export interface NearbyPoi {
  poi_type: string; // OSM POI category, e.g. "school" / "metro_stop" / "kindergarten"
  name: string | null; // POI name if known
  distance_m: number;
}

export interface LocationIndexResponse {
  status: "ok" | "out_of_coverage" | "insufficient_data";
  location_index_pct: number | null;
  local_median_price_per_m2: number | null;
  city_median_price_per_m2: number | null;
  sample_size: number;
  radius_m: number;
  nearby_poi: NearbyPoi[];
  poi_status: "ok" | "unavailable";
}
