import datetime as dt
from typing import Any, Literal

from pydantic import BaseModel, Field

# ── #105 Phase 5: Recent permits schemas ──────────────────────────────────────


class RecentPermit(BaseModel):
    """Одно строительное разрешение (РНС или РВЭ) из ekburg_construction_permits."""

    permit_type: str
    permit_number: str
    issue_date: str | None
    developer_name: str | None
    developer_inn: str | None
    object_name: str | None
    object_type: str | None
    construction_address: str | None
    total_area_sqm: float | None


class PermitsSummary(BaseModel):
    """Агрегированная сводка по разрешениям в квартале."""

    rns_count: int
    rve_count: int
    rns_total_area_sqm: float
    by_developer: list[dict[str, Any]]


# ── Connection points schemas (issue #115) ────────────────────────────────────


class EngineeringStructure(BaseModel):
    name: str | None
    type: str | None
    cad_num: str | None
    distance_to_boundary_m: float
    geometry_geojson: dict[str, Any]
    readable_address: str | None
    raw_props: dict[str, Any]
    source: str


class ZouitOverlap(BaseModel):
    reg_numb_border: str | None
    type_zone: str | None
    subcategory: int | None
    intersects_parcel: bool
    geometry_geojson: dict[str, Any]
    raw_props: dict[str, Any]
    source: str


class ConnectionPointsSummary(BaseModel):
    nearest_structure_distance_m: float | None
    in_protection_zone: bool
    protection_zones_intersecting: int
    total_structures_in_radius: int


class ConnectionPointsResponse(BaseModel):
    engineering_structures: list[EngineeringStructure]
    zouit_engineering_overlaps: list[ZouitOverlap]
    summary: ConnectionPointsSummary
    dump_available: bool
    dump_fetched_at: str | None


# ── NSPD Risk Zones schemas (issue #94 TIER 3) ───────────────────────────────


class RiskZone(BaseModel):
    """Одна риск-зона НСПД (TIER 3), пересекающая участок."""

    layer: str  # e.g. "risk_flooding", "risk_landslide"
    subtype: str | None  # человекочитаемое название из NSPD properties
    geom_wkt: str | None  # WKT геометрии risk-зоны в EPSG:4326
    intersection_area_sqm: float | None  # площадь пересечения с участком, м²


# ── NSPD Opportunity + Red Lines schemas (issue #94 TIER 4) ──────────────────


class OpportunityParcel(BaseModel):
    """TIER 4: opportunity ЗУ вблизи участка (auction/scheme/free/future/oopt).

    Поля:
        layer: тип opportunity — "auction_parcels" | "scheme_parcels" |
               "free_parcels" | "future_parcels" | "oopt"
        cad_num: кадастровый номер ЗУ (если доступен в NSPD properties)
        distance_m: расстояние от centroid анализируемого участка до ЗУ, м
        geom_wkt: WKT геометрии ЗУ в EPSG:4326
    """

    layer: str
    cad_num: str | None
    distance_m: float | None
    geom_wkt: str | None


class RedLine(BaseModel):
    """TIER 4: красная линия застройки (NSPD layer 879243).

    Поля:
        geom_wkt: WKT геометрии линии в EPSG:4326
        intersection_length_m: длина пересечения линии с участком, м
                               (null если только nearby, не intersect)
        distance_m: расстояние от ближайшей точки линии до участка, м
                    (null если линия пересекает участок)

    UI-семантика:
        intersection_length_m != null → красная линия ПЕРЕСЕКАЕТ участок (ALERT)
        distance_m != null → красная линия рядом (warning)
    """

    geom_wkt: str | None
    intersection_length_m: float | None  # null если только nearby, не intersect
    distance_m: float | None  # null если intersect


# ── Parcel search/detail schemas ──────────────────────────────────────────────


class ParcelFilter(BaseModel):
    vri: list[str] | None = None
    area_min_sqm: float | None = None
    area_max_sqm: float | None = None
    region: str | None = None
    polygon_geojson: dict[str, Any] | None = None
    text_search: str | None = None


class ScoringWeights(BaseModel):
    distance_to_center: float = 0.4
    poi_density: float = 0.3
    neighborhood_price: float = 0.3


class ParcelSearchRequest(BaseModel):
    filters: ParcelFilter = Field(default_factory=ParcelFilter)
    weights: ScoringWeights = Field(default_factory=ScoringWeights)
    limit: int = 50


class ScoreBreakdown(BaseModel):
    total: float
    distance_to_center: float
    poi_density: float
    neighborhood_price: float


class ParcelSummary(BaseModel):
    id: str
    cadastral_number: str
    area_sqm: float
    vri: str
    address: str | None
    score: ScoreBreakdown


class ParcelSearchResponse(BaseModel):
    items: list[ParcelSummary]
    total: int


class ParcelDetail(ParcelSummary):
    geometry_geojson: dict[str, Any]
    enrichment: dict[str, Any]


# ── Competitors endpoint schemas ──────────────────────────────────────────────

TimeWindow = Literal["last_month", "last_quarter", "last_year"]
ObjClassFilter = Literal["economy", "comfort", "business"]


class CompetitorsRequest(BaseModel):
    radius_km: float = Field(default=1.0, ge=0.1, le=1.5)
    time_window: TimeWindow = "last_quarter"
    obj_class_filter: ObjClassFilter | None = None
    exclude_obj_ids: list[int] = Field(default_factory=list)


class Competitor(BaseModel):
    obj_id: int
    comm_name: str | None
    dev_name: str | None
    obj_class: str | None
    distance_m: float
    lat: float
    lng: float
    stage: str | None
    site_status: str | None = None
    ready_dt: dt.date | None = None
    flats_total: int | None
    flats_sold: int | None
    sold_pct: float | None
    velocity_per_month: float
    avg_price_per_m2: float | None
    is_active: bool


class CompetitorsSummary(BaseModel):
    total_competitors: int
    active_count: int
    weighted_avg_velocity: float
    radius_km: float
    time_window: str


class CompetitorsResponse(BaseModel):
    competitors: list[Competitor]
    summary: CompetitorsSummary


# ── Layout analysis (Issue #113) ───────────────────────────────────────────


class LayoutSignature(BaseModel):
    """Минимальная сигнатура планировки = (room_bucket, area_bin).

    Phase 2.1: layout_type/balcony_count в БД нет, ждут B2B Объектив (#52).
    """

    room_bucket: Literal["studio", "1", "2", "3", "4+"]
    area_bin: Literal["<25", "25-40", "40-60", "60-80", "80-100", "100+"]


class BestLayoutsRequest(BaseModel):
    """Параметры запроса top-планировок в радиусе вокруг участка."""

    radius_km: float = Field(default=1.0, ge=0.1, le=1.5)
    time_window: Literal["last_month", "last_quarter", "last_year"] = "last_quarter"
    filter_competitor_obj_ids: list[int] | None = None
    exclude_competitor_obj_ids: list[int] = Field(default_factory=list)
    min_velocity_per_month: float = Field(default=0.5, ge=0.0, le=100.0)
    obj_class_filter: Literal["economy", "comfort", "business"] | None = None
    target_total_flats: int | None = Field(default=None, ge=1, le=10000)


class TopLayoutRow(BaseModel):
    """Одна строка top-planirovok ranking'а."""

    rank: int
    room_bucket: str
    area_bin: str
    signature: str
    competitor_obj_ids: list[int]
    competitor_count: int
    total_sold_in_window: int
    velocity_per_month: float
    avg_price_per_m2_rub: float | None  # NULL если objective не покрывает obj
    avg_area_m2: float
    supply_units_in_radius: int
    sold_pct_of_supply: float | None  # NULL если supply=0; clamped at 100.0
    is_oversold: bool  # True когда raw sum_deals/supply > 100% (несопоставимые окна)


class LayoutTzMixRow(BaseModel):
    """Строка рекомендации unit-mix для ТЗ."""

    room_bucket: str
    pct: int  # 0..100, sum total = 100
    abs_units: int | None  # NULL если target_total_flats не задан
    avg_target_area_m2: float | None


class LayoutTzRecommendation(BaseModel):
    """Рекомендация для ТЗ на проектирование."""

    rationale_text: str
    mix: list[LayoutTzMixRow]
    weighted_avg_price_per_m2_rub: float | None
    based_on_obj_count: int
    based_on_total_deals: int
    data_window_start: dt.date
    data_window_end: dt.date
    # Fix SF-09 review: True если pathological case — все bucket'ы выше cap,
    # redistribute невозможен. Frontend использует для отображения warning banner.
    cap_skipped: bool = False


class LayoutDataQuality(BaseModel):
    """Метаданные качества данных (coverage)."""

    objects_with_velocity_data: int
    objects_total_in_radius: int
    velocity_coverage_pct: float
    confidence: Literal["high", "medium", "low"]


class BestLayoutsResponse(BaseModel):
    """Ответ /best-layouts endpoint'а."""

    top_layouts: list[TopLayoutRow]
    recommendation_for_tz: LayoutTzRecommendation
    data_quality: LayoutDataQuality


# ── Analyze endpoint market price (#33) ──────────────────────────────────────


class MarketPrice(BaseModel):
    """Ценовая статистика квартала из mv_quarter_price_per_m2 (Issue #33).

    Источник: rosreestr_deals, фильтр realestate_type_code='002001003000' (ДДУ),
    скользящее окно 24 мес., 30K–800K руб/м², HAVING >= 3 сделок.
    """

    p25: float | None = None
    median: float | None = None
    p75: float | None = None
    mean: float | None = None
    deals_count: int = 0
    median_6m: float | None = None
    median_12m: float | None = None
    median_24m: float | None = None
    last_deal_date: str | None = None  # ISO date
    source: Literal["quarter_mv", "no_data"] = "no_data"


# ── Analyze endpoint parcel meta (#29 G2) ────────────────────────────────────


class ParcelMeta(BaseModel):
    """Кадастровые метаданные участка из cad_parcels (#29 G2).

    Поля берутся из NSPD bulk-ingest (issue #168). Строки сырые, без нормализации.
    """

    permitted_use: str | None = None  # permitted_use_established_by_document
    land_category: str | None = None  # land_record_category_type
    land_subtype: str | None = None  # land_record_subtype
    cad_cost: float | None = None  # cost_value (кадастровая стоимость, руб.)
    source: str = "cad_parcels"


# ── Analyze endpoint inline weights (#201) ────────────────────────────────────


class AnalyzeRequest(BaseModel):
    """Опциональное тело запроса POST /analyze.

    Позволяет передать inline POI-веса напрямую в запросе без сохранения
    профиля. Если задан weights — применяется с наивысшим приоритетом
    (выше profile_id и user default).
    """

    weights: dict[str, float] | None = Field(
        default=None,
        description=(
            "Inline POI weights override (категория → weight). "
            "Если задан — применяется к scoring, без обязательного profile save. "
            "Validated против ALLOWED_CATEGORIES + MIN_WEIGHT/MAX_WEIGHT."
        ),
    )


# ── SF-B1: by-bbox map entry schemas (#307) ──────────────────────────────────


class ParcelMapMarker(BaseModel):
    """Один маркер участка на карте — облегчённый ответ для bbox-запроса.

    status берётся из parcel_user_status если передан user_id, иначе null.
    last_analysis_date — placeholder до реализации B2 auth.
    """

    cad_num: str
    centroid_lat: float
    centroid_lon: float
    area_m2: float | None
    land_category: str | None
    status: str | None  # 'in_work' | 'favorite' | 'dismissed' | null
    last_analysis_date: str | None  # placeholder, real after B2 auth


class ParcelBboxResponse(BaseModel):
    """Ответ GET /parcels/by-bbox."""

    parcels: list[ParcelMapMarker]
    count: int
    limit: int
    bbox_area_km2: float
