from __future__ import annotations

import os
from dataclasses import asdict, dataclass, field, fields, replace
from typing import Any, Dict

import tomllib
import tomli_w


DEFAULT_CONFIG_PATH = "geodata_config.toml"


@dataclass
class RawConfig:
    dgm1_dir: str = "raw/dgm1"
    dop20_dir: str = "raw/dop20/jp2"
    citygml_lod1_dir: str = "raw/citygml/lod1"
    citygml_lod2_dir: str = "raw/citygml/lod2"


@dataclass
class ArchiveConfig:
    dgm1_dir: str = "archive/dgm1"
    dop20_dir: str = "archive/dop20"
    citygml_lod1_dir: str = "archive/citygml/lod1"
    citygml_lod2_dir: str = "archive/citygml/lod2"


@dataclass
class WorkConfig:
    work_dir: str = "work"
    heightmap_vrt: str = "work/dgm.vrt"
    ortho_vrt: str = "work/dop.vrt"


@dataclass
class ExportConfig:
    heightmap_dir: str = "export_unity/height_png16"
    ortho_dir: str = "export_unity/ortho_jpg"
    manifest_path: str = "export_unity/tile_index.csv"


@dataclass
class HeightmapConfig:
    out_res: int = 1025
    resample: str = "bilinear"
    tile_size_m: int = 1000
    use_tile_minmax: bool = True


@dataclass
class RiverErosionProfileConfig:
    order_field: str = "ORD_STRA"
    invert_order: bool = False
    invert_max_order: int = 10
    min_order: int = 1
    width_base_m: float = 8.0
    width_per_order_m: float = 6.0
    width_min_m: float = 4.0
    width_max_m: float = 90.0
    depth_base_m: float = 0.2
    depth_per_order_m: float = 0.44
    depth_min_m: float = 0.2
    depth_max_m: float = 6.0
    smooth_sigma_m: float = 3.0
    fallback_depth_m: float = 2.0
    flow_distance_field: str = "DIST_DN_KM"
    flow_slope_m_per_km: float = 0.0


@dataclass
class RiverErosionLidarConfig:
    enabled: bool = True
    source_dir: str = "raw/bdom20rgbi"
    classification_water: int = 9
    depth_m: float = 3.5
    bank_slope_sigma: float = 2.0
    fill_holes_radius: int = 3


def _default_river_profile_vr() -> "RiverErosionProfileConfig":
    return RiverErosionProfileConfig(
        order_field="ORD_STRA",
        invert_order=False,
        invert_max_order=10,
        min_order=1,
        width_base_m=8.0,
        width_per_order_m=6.0,
        width_min_m=4.0,
        width_max_m=90.0,
        depth_base_m=0.2,
        depth_per_order_m=0.44,
        depth_min_m=0.2,
        depth_max_m=6.0,
        smooth_sigma_m=3.0,
        fallback_depth_m=2.0,
        flow_distance_field="DIST_DN_KM",
        flow_slope_m_per_km=0.0,
    )


def _default_river_profile_server() -> "RiverErosionProfileConfig":
    return RiverErosionProfileConfig(
        order_field="ORD_FLOW",
        invert_order=True,
        invert_max_order=10,
        min_order=2,
        width_base_m=5.0,
        width_per_order_m=7.0,
        width_min_m=3.0,
        width_max_m=60.0,
        depth_base_m=0.4,
        depth_per_order_m=0.3,
        depth_min_m=0.2,
        depth_max_m=4.0,
        smooth_sigma_m=1.5,
    )


@dataclass
class RiverErosionConfig:
    enabled: bool = True
    source_path: str = "raw/HydroRIVERS_v10_eu_shp/HydroRIVERS_v10_eu.shp"
    source_layer: str = "HydroRIVERS_v10_eu"
    output_dir: str = "export_unity/height_png16_river"
    manifest_vr: str = "export_unity/tile_index_river_vr.csv"
    manifest_server: str = "export_unity/tile_index_river_server.csv"
    vr: RiverErosionProfileConfig = field(default_factory=_default_river_profile_vr)
    server: RiverErosionProfileConfig = field(default_factory=_default_river_profile_server)
    lidar: RiverErosionLidarConfig = field(default_factory=RiverErosionLidarConfig)


@dataclass
class OrthoConfig:
    out_res: int = 2048
    jpeg_quality: int = 90
    apply_water_mask: bool = False
    water_blend: float = 0.85
    water_fallback_rgb: tuple[float, float, float] = (30.0, 50.0, 60.0)
    water_mask_threshold: float = 0.1
    river_dir: str = "export_unity/ortho_jpg_river"
    water_color_mode: str = "median"


@dataclass
class TileKeyConfig:
    tile_size_x: float = 1000.0
    tile_size_y: float = 1000.0
    overlap_x: float = 0.5
    overlap_y: float = 0.5
    enabled: bool = True


@dataclass
class BuildingConfig:
    out_dir: str = "export_unity/buildings_tiles"
    work_cityjson_dir: str = "work/cityjson_lod2"
    work_rebased_dir: str = "work/cityjson_lod2_local"
    work_glb_dir: str = "work/buildings_glb_tmp"
    triangle_budget_min: int = 200000
    triangle_budget_max: int = 350000
    roof_unlit: bool = True


@dataclass
class TreeConfig:
    csv_dir: str = "export_unity/trees"
    glb_dir: str = "export_unity/trees_tiles"
    proxy_library: str = "export_unity/tree_proxies.glb"
    grid_res_m: float = 2.0
    min_height_m: float = 2.0
    max_trees: int = 5000
    chunk_grid: int = 4
    proxy_variants: int = 16
    proxy_min_tris: int = 120
    proxy_max_tris: int = 180
    building_buffer_m: float = 1.5
    instancing: bool = False


@dataclass
class PointCloudConfig:
    """Configuration for point cloud data sources."""
    lpg_dir: str = "raw/lpg"
    lpo_dir: str = "raw/lpo"
    lpolpg_dir: str = "raw/lpolpg"
    bdom_dir: str = "raw/bdom20rgbi"
    dom1_dir: str = "raw/dom1"
    chunk_size: int = 5_000_000
    use_lpg_validation: bool = True
    use_lpo_refinement: bool = True


@dataclass
class EnhancedHeightmapConfig:
    """Configuration for enhanced heightmap export with LPG validation."""
    lpg_validation_threshold_m: float = 0.5
    export_quality_map: bool = True
    quality_map_dir: str = "export_unity/height_quality"


@dataclass
class EnhancedBuildingConfig:
    """Configuration for enhanced building export with point cloud fusion."""
    out_dir: str = "export_unity/buildings_enhanced"
    ransac_threshold_m: float = 0.15
    ransac_iterations: int = 1000
    max_roof_planes: int = 6
    min_plane_inliers: int = 50
    use_bdom_roof: bool = True
    use_lpo_refinement: bool = True
    use_dop20_textures: bool = True
    fallback_to_citygml: bool = True


@dataclass
class StreetFurnitureConfig:
    """Configuration for street furniture detection."""
    enabled: bool = True
    csv_dir: str = "export_unity/street_furniture"
    min_height_m: float = 0.3
    max_height_m: float = 8.0
    min_confidence: float = 0.6
    use_lpo_refinement: bool = True
    lamp_height_range: tuple = (4.0, 8.0)
    bench_height_range: tuple = (0.4, 1.0)
    sign_height_range: tuple = (2.0, 4.0)
    bollard_height_range: tuple = (0.5, 1.2)


@dataclass
class EnhancedTreeConfig:
    """Configuration for enhanced tree detection."""
    csv_dir: str = "export_unity/trees_enhanced"
    glb_dir: str = "export_unity/trees_enhanced_tiles"
    exclude_furniture: bool = True
    use_lpo_refinement: bool = True
    sample_canopy_color: bool = True
    lpo_search_radius_m: float = 5.0
    canopy_sample_radius_factor: float = 0.5


@dataclass
class Config:
    raw: RawConfig = field(default_factory=RawConfig)
    archives: ArchiveConfig = field(default_factory=ArchiveConfig)
    work: WorkConfig = field(default_factory=WorkConfig)
    export: ExportConfig = field(default_factory=ExportConfig)
    heightmap: HeightmapConfig = field(default_factory=HeightmapConfig)
    river_erosion: RiverErosionConfig = field(default_factory=RiverErosionConfig)
    ortho: OrthoConfig = field(default_factory=OrthoConfig)
    tile_key: TileKeyConfig = field(default_factory=TileKeyConfig)
    buildings: BuildingConfig = field(default_factory=BuildingConfig)
    trees: TreeConfig = field(default_factory=TreeConfig)
    # Enhanced pipeline configs
    pointcloud: PointCloudConfig = field(default_factory=PointCloudConfig)
    heightmap_enhanced: EnhancedHeightmapConfig = field(default_factory=EnhancedHeightmapConfig)
    buildings_enhanced: EnhancedBuildingConfig = field(default_factory=EnhancedBuildingConfig)
    street_furniture: StreetFurnitureConfig = field(default_factory=StreetFurnitureConfig)
    trees_enhanced: EnhancedTreeConfig = field(default_factory=EnhancedTreeConfig)

    @classmethod
    def default(cls) -> "Config":
        return cls()

    @classmethod
    def load(cls, path: str = DEFAULT_CONFIG_PATH) -> "Config":
        with open(path, "rb") as f:
            data = tomllib.load(f)
        return cls.from_dict(data)

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "Config":
        return cls(
            raw=RawConfig(**_filter_kwargs(RawConfig, data.get("raw", {}))),
            archives=ArchiveConfig(**_filter_kwargs(ArchiveConfig, data.get("archives", {}))),
            work=WorkConfig(**_filter_kwargs(WorkConfig, data.get("work", {}))),
            export=ExportConfig(**_filter_kwargs(ExportConfig, data.get("export", {}))),
            heightmap=HeightmapConfig(**_filter_kwargs(HeightmapConfig, data.get("heightmap", {}))),
            river_erosion=_river_erosion_from_dict(data.get("river_erosion", {})),
            ortho=OrthoConfig(**_filter_kwargs(OrthoConfig, data.get("ortho", {}))),
            tile_key=TileKeyConfig(**_filter_kwargs(TileKeyConfig, data.get("tile_key", {}))),
            buildings=BuildingConfig(**_filter_kwargs(BuildingConfig, data.get("buildings", {}))),
            trees=TreeConfig(**_filter_kwargs(TreeConfig, data.get("trees", {}))),
            # Enhanced pipeline configs (with defaults for backward compat)
            pointcloud=PointCloudConfig(**_filter_kwargs(PointCloudConfig, data.get("pointcloud", {}))),
            heightmap_enhanced=EnhancedHeightmapConfig(**_filter_kwargs(
                EnhancedHeightmapConfig,
                data.get("heightmap_enhanced", {}),
            )),
            buildings_enhanced=EnhancedBuildingConfig(**_filter_kwargs(
                EnhancedBuildingConfig,
                data.get("buildings_enhanced", {}),
            )),
            street_furniture=StreetFurnitureConfig(**_filter_kwargs(
                StreetFurnitureConfig,
                _coerce_ranges(data.get("street_furniture", {})),
            )),
            trees_enhanced=EnhancedTreeConfig(**_filter_kwargs(
                EnhancedTreeConfig,
                data.get("trees_enhanced", {}),
            )),
        )

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)

    def save(self, path: str = DEFAULT_CONFIG_PATH) -> None:
        payload = tomli_w.dumps(self.to_dict())
        with open(path, "w", encoding="utf-8") as f:
            f.write(payload)

    def with_overrides(self, raw_dgm1_path: str | None = None, raw_dop20_path: str | None = None) -> "Config":
        cfg = self
        if raw_dgm1_path:
            cfg = replace(cfg, raw=replace(cfg.raw, dgm1_dir=raw_dgm1_path))
        if raw_dop20_path:
            cfg = replace(cfg, raw=replace(cfg.raw, dop20_dir=raw_dop20_path))
        return cfg


def ensure_default_config(path: str = DEFAULT_CONFIG_PATH) -> Config:
    if not os.path.exists(path):
        cfg = Config.default()
        cfg.save(path)
        return cfg
    return Config.load(path)


def _filter_kwargs(cls: type, data: Dict[str, Any]) -> Dict[str, Any]:
    if not isinstance(data, dict):
        return {}
    allowed = {field.name for field in fields(cls)}
    return {key: value for key, value in data.items() if key in allowed}


def _coerce_ranges(data: Dict[str, Any]) -> Dict[str, Any]:
    if not isinstance(data, dict):
        return {}
    out = dict(data)
    for key in ("lamp_height_range", "bench_height_range", "sign_height_range", "bollard_height_range"):
        value = out.get(key)
        if isinstance(value, list):
            out[key] = tuple(value)
    value = out.get("water_fallback_rgb")
    if isinstance(value, list):
        out["water_fallback_rgb"] = tuple(value)
    return out


def _river_erosion_from_dict(data: Dict[str, Any]) -> RiverErosionConfig:
    if not isinstance(data, dict):
        return RiverErosionConfig()
    base = _filter_kwargs(RiverErosionConfig, data)
    base.pop("vr", None)
    base.pop("server", None)
    base.pop("lidar", None)
    vr_cfg = RiverErosionProfileConfig(**_filter_kwargs(RiverErosionProfileConfig, data.get("vr", {})))
    server_cfg = RiverErosionProfileConfig(**_filter_kwargs(RiverErosionProfileConfig, data.get("server", {})))
    lidar_cfg = RiverErosionLidarConfig(**_filter_kwargs(RiverErosionLidarConfig, data.get("lidar", {})))
    return RiverErosionConfig(**base, vr=vr_cfg, server=server_cfg, lidar=lidar_cfg)
