from __future__ import annotations

from collections import Counter
from dataclasses import dataclass
from pathlib import Path
import re
import math
from typing import Any


ROLE_LABELS: dict[str, str] = {
    "ground_line": "断面地形线/河床线",
    "water_level": "计算水位线",
    "design_water_level": "设计洪水位线",
    "backwater_line": "壅水线",
    "scour_line": "冲刷线",
    "bridge_outline": "桥梁/水工建筑物轮廓",
    "pier": "桥墩/墩台",
    "station_axis": "桩号/断面轴线",
    "label": "文字标注",
    "table": "成果表格",
}

REQUIRED_ROLES = ("ground_line",)
SUPPORTED_CAD_EXTENSIONS = {".dxf"}
RECOGNIZED_BUT_UNSUPPORTED_EXTENSIONS = {".dwg"}
DRAWABLE_ENTITY_TYPES = {
    "LINE",
    "LWPOLYLINE",
    "POLYLINE",
    "VERTEX",
    "ARC",
    "CIRCLE",
    "TEXT",
    "MTEXT",
    "POINT",
    "SPLINE",
}

RECOMMENDED_LAYER_MAP: dict[str, str] = {
    "HTZD_SECTION_GROUND": "ground_line",
    "HTZD_SECTION_WATER_LEVEL": "water_level",
    "HTZD_SECTION_DESIGN_WL": "design_water_level",
    "HTZD_SECTION_BACKWATER": "backwater_line",
    "HTZD_SECTION_SCOUR": "scour_line",
    "HTZD_STRUCTURE_OUTLINE": "bridge_outline",
    "HTZD_STRUCTURE_PIER": "pier",
    "HTZD_SECTION_AXIS": "station_axis",
    "HTZD_TEXT_LABEL": "label",
    "HTZD_RESULT_TABLE": "table",
}

IMPORT_MODES: dict[str, dict[str, str]] = {
    "smart": {
        "name": "智能识别模式",
        "summary": "自动分析图层名称、线型、颜色和文字特征，给出可确认的断面要素映射。",
        "best_for": "已有图纸未按慧通智达模板绘制，但图层名称仍有工程含义。",
    },
    "manual": {
        "name": "手动映射模式",
        "summary": "用户指定哪一层或哪类对象代表地形线、水位线、冲刷线等要素。",
        "best_for": "图纸图层混乱、历史图纸、外单位图纸或扫描后矢量化图纸。",
    },
    "standard": {
        "name": "标准模板模式",
        "summary": "使用慧通智达推荐图层名时可一键识别，提高批量导入准确率。",
        "best_for": "新绘图纸、长期项目模板、多人协作统一出图。",
    },
}

_ROLE_KEYWORDS: tuple[tuple[str, tuple[str, ...]], ...] = (
    ("design_water_level", ("设计洪水位", "设计水位", "设计wl", "deswl", "designwl", "design_wl")),
    ("backwater_line", ("壅水", "回水", "backwater", "afflux")),
    ("scour_line", ("冲刷", "冲淤", "scour", "erosion")),
    ("water_level", ("水位", "洪水位", "水面线", "water", "wl")),
    ("ground_line", ("地形线", "地面线", "河床", "断面线", "天然断面", "section", "ground", "terrain", "bed")),
    ("bridge_outline", ("桥梁", "桥", "建筑物", "构造物", "轮廓", "bridge", "structure")),
    ("pier", ("桥墩", "墩台", "墩", "pier", "abutment")),
    ("station_axis", ("桩号", "轴线", "断面轴", "坐标", "chainage", "station", "axis")),
    ("label", ("文字", "标注", "注记", "text", "label", "anno")),
    ("table", ("表格", "成果表", "统计表", "table")),
)


@dataclass(frozen=True)
class LayerGuess:
    layer: str
    role: str
    role_label: str
    confidence: float
    reason: str


def available_import_modes() -> dict[str, Any]:
    return {
        "ok": True,
        "modes": IMPORT_MODES,
        "roles": ROLE_LABELS,
        "required_roles": list(REQUIRED_ROLES),
        "recommended_layers": [
            {"layer": layer, "role": role, "role_label": ROLE_LABELS[role]}
            for layer, role in RECOMMENDED_LAYER_MAP.items()
        ],
        "principle": "使用标准图层可提高自动识别准确率；非标准图纸也可通过手动映射导入。",
    }


def normalize_layer_name(name: Any) -> str:
    text = str(name or "").strip()
    text = re.sub(r"\s+", "", text)
    return text


def _layer_text(layer: Any) -> str:
    if isinstance(layer, dict):
        for key in ("name", "layer", "layerName", "title"):
            if layer.get(key):
                return normalize_layer_name(layer.get(key))
    return normalize_layer_name(layer)


def _infer_role(layer_name: str) -> LayerGuess:
    lower_name = layer_name.lower()
    upper_name = layer_name.upper()
    if upper_name in RECOMMENDED_LAYER_MAP:
        role = RECOMMENDED_LAYER_MAP[upper_name]
        return LayerGuess(
            layer=layer_name,
            role=role,
            role_label=ROLE_LABELS[role],
            confidence=0.98,
            reason="匹配慧通智达推荐图层名。",
        )

    for role, keywords in _ROLE_KEYWORDS:
        for keyword in keywords:
            if keyword.lower() in lower_name:
                return LayerGuess(
                    layer=layer_name,
                    role=role,
                    role_label=ROLE_LABELS[role],
                    confidence=0.78 if role in REQUIRED_ROLES else 0.72,
                    reason=f"图层名包含“{keyword}”。",
                )

    return LayerGuess(layer=layer_name, role="unknown", role_label="待人工确认", confidence=0.0, reason="未匹配到断面要素关键词。")


def _coerce_layers(layers: Any) -> list[str]:
    if layers is None:
        return []
    if isinstance(layers, str):
        items = re.split(r"[\n,;；，]+", layers)
        return [normalize_layer_name(item) for item in items if normalize_layer_name(item)]
    if isinstance(layers, (list, tuple)):
        result: list[str] = []
        for layer in layers:
            text = _layer_text(layer)
            if text:
                result.append(text)
        return result
    return []


def analyze_layers(layers: Any, mode: str = "smart") -> dict[str, Any]:
    mode = mode if mode in IMPORT_MODES else "smart"
    layer_names = _coerce_layers(layers)
    guesses = [_infer_role(layer) for layer in layer_names]
    mapped_roles = {guess.role for guess in guesses if guess.role != "unknown"}
    missing_required = [role for role in REQUIRED_ROLES if role not in mapped_roles]
    missing_standard = [
        {"layer": layer, "role": role, "role_label": ROLE_LABELS[role]}
        for layer, role in RECOMMENDED_LAYER_MAP.items()
        if role not in mapped_roles
    ]

    if mode == "standard":
        message = "已按慧通智达推荐图层名识别；缺失项可补绘，也可切换手动映射。"
        requires_manual = bool(missing_required or missing_standard)
    elif mode == "manual":
        message = "请手动指定每类断面要素对应的图层；非标准图纸不需要改图层名。"
        requires_manual = True
    else:
        message = "已完成智能识别，请确认低置信度和未识别图层；无法确认时切换手动映射。"
        requires_manual = bool(missing_required or any(guess.confidence < 0.65 for guess in guesses))

    return {
        "ok": True,
        "mode": mode,
        "mode_name": IMPORT_MODES[mode]["name"],
        "layers": layer_names,
        "guesses": [guess.__dict__ for guess in guesses],
        "missing_required": [{"role": role, "role_label": ROLE_LABELS[role]} for role in missing_required],
        "missing_standard_layers": missing_standard,
        "requires_manual_confirmation": requires_manual,
        "message": message,
        "principle": "使用标准图层可提高自动识别准确率；非标准图纸也可通过手动映射导入。",
    }


def validate_manual_mapping(layers: Any, mapping: dict[str, Any] | None) -> dict[str, Any]:
    layer_names = _coerce_layers(layers)
    layer_set = set(layer_names)
    mapping = mapping or {}
    accepted: dict[str, str] = {}
    errors: list[str] = []

    for role, layer in mapping.items():
        role = str(role)
        layer_name = normalize_layer_name(layer)
        if role not in ROLE_LABELS:
            errors.append(f"未知要素类型：{role}")
            continue
        if not layer_name:
            continue
        if layer_set and layer_name not in layer_set:
            errors.append(f"图层“{layer_name}”不在当前图层列表中。")
            continue
        accepted[role] = layer_name

    for role in REQUIRED_ROLES:
        if role not in accepted:
            errors.append(f"必须指定“{ROLE_LABELS[role]}”。")

    return {
        "ok": not errors,
        "mapping": accepted,
        "errors": errors,
        "message": "映射可用于断面导入。" if not errors else "映射不完整，请补齐后再导入。",
        "principle": "不强制固定图层命名；标准图层只用于提高自动识别准确率。",
    }


def _read_cad_text(path: Path) -> str:
    data = path.read_bytes()
    for encoding in ("utf-8", "gbk", "latin-1"):
        try:
            return data.decode(encoding)
        except UnicodeDecodeError:
            continue
    return data.decode("utf-8", errors="ignore")


def _dxf_pairs(text: str) -> list[tuple[str, str]]:
    lines = [line.strip() for line in text.splitlines()]
    pairs: list[tuple[str, str]] = []
    index = 0
    while index + 1 < len(lines):
        pairs.append((lines[index], lines[index + 1]))
        index += 2
    return pairs


def _parse_dxf_entities(path: Path) -> list[dict[str, Any]]:
    pairs = _dxf_pairs(_read_cad_text(path))
    entities: list[dict[str, Any]] = []
    current: dict[str, Any] | None = None

    def flush() -> None:
        if not current:
            return
        entity_type = str(current.get("type", "")).upper()
        if entity_type and entity_type not in {"SECTION", "ENDSEC", "TABLE", "ENDTAB", "EOF"}:
            entities.append(current.copy())

    for code, value in pairs:
        if code == "0":
            flush()
            current = {"type": value.upper(), "layer": "0", "codes": []}
            continue
        if current is None:
            continue
        current["codes"].append((code, value))
        if code == "8":
            current["layer"] = normalize_layer_name(value) or "0"
        elif current.get("type") == "LAYER" and code == "2":
            current["layer_name"] = normalize_layer_name(value)
    flush()
    return entities


def _collect_layers_from_entities(entities: list[dict[str, Any]]) -> list[dict[str, Any]]:
    counter: Counter[str] = Counter()
    table_layers: set[str] = set()
    for entity in entities:
        entity_type = str(entity.get("type", "")).upper()
        if entity_type == "LAYER":
            layer_name = normalize_layer_name(entity.get("layer_name"))
            if layer_name:
                table_layers.add(layer_name)
            continue
        layer_name = normalize_layer_name(entity.get("layer"))
        if layer_name and entity_type in DRAWABLE_ENTITY_TYPES:
            counter[layer_name] += 1
    for layer_name in table_layers:
        counter.setdefault(layer_name, 0)
    return [{"name": name, "entity_count": count} for name, count in sorted(counter.items())]


def _entity_points(entity: dict[str, Any]) -> list[tuple[float, float]]:
    entity_type = str(entity.get("type", "")).upper()
    codes = entity.get("codes", [])
    points: list[tuple[float, float]] = []
    if entity_type == "LINE":
        values = {code: value for code, value in codes}
        try:
            return [(float(values["10"]), float(values["20"])), (float(values["11"]), float(values["21"]))]
        except (KeyError, TypeError, ValueError):
            return []
    if entity_type in {"LWPOLYLINE", "VERTEX"}:
        pending_x: float | None = None
        for code, value in codes:
            try:
                number = float(value)
            except (TypeError, ValueError):
                continue
            if code == "10":
                pending_x = number
            elif code == "20" and pending_x is not None:
                points.append((pending_x, number))
                pending_x = None
    return points


def extract_section_profile(path: str | Path, ground_layer: str | None = None) -> dict[str, Any]:
    # @hook SECTION-CAD-IMPORT-FILE 20260817
    # Extracts simple DXF section geometry from the selected mapped ground layer.
    file_path = Path(path).expanduser()
    if not file_path.exists():
        return {"ok": False, "message": "CAD/DXF 文件不存在。", "profile": []}
    if file_path.suffix.lower() != ".dxf":
        return {"ok": False, "message": "当前几何提取先支持 DXF 文件。", "profile": []}

    target_layer = normalize_layer_name(ground_layer)
    if not target_layer:
        return {"ok": False, "message": "请明确选择地形线图层，不能将水位线、标注等所有图层合并为地形。", "profile": []}
    pairs = _dxf_pairs(_read_cad_text(file_path))
    declared_units = 0
    for i, (code, value) in enumerate(pairs[:-1]):
        if code == '9' and value == '$INSUNITS' and pairs[i+1][0] == '70':
            try:
                declared_units = int(pairs[i+1][1])
            except ValueError:
                return {'ok':False,'message':'DXF单位声明无效，请在CAD中检查图纸单位。','profile':[]}
    if declared_units not in (0, 6):
        return {'ok':False,'message':f'DXF声明的单位代码为{declared_units}，并非米。请在CAD中核实并转换坐标及单位为米后导入，软件不会猜测比例。','profile':[]}
    entities = _parse_dxf_entities(file_path)
    raw_points: list[tuple[float, float]] = []
    for entity in entities:
        layer_name = normalize_layer_name(entity.get("layer"))
        if target_layer and layer_name != target_layer:
            continue
        if entity.get('type') in {'LINE','LWPOLYLINE','VERTEX'}:
            for code,value in entity.get('codes', []):
                if code in {'10','20','11','21','30','31','42'}:
                    try:
                        numeric = float(value)
                        if not math.isfinite(numeric): raise ValueError()
                    except ValueError:
                        return {'ok':False,'message':'地形实体存在无效坐标或凸度，不能跳过错误点继续计算。','profile':[]}
                    if code in {'30','31'} and numeric != 0:
                        return {'ok':False,'message':'当前输入需为XY二维断面（X为横距、Y为高程）；三维测量线请先投影为二维断面，不会忽略Z坐标。','profile':[]}
        if any(code == '42' and float(value) != 0 for code,value in entity.get('codes', [])):
            return {"ok":False,"message":"地形线含圆弧凸度，需在CAD按测量精度离散为折线后导入，不能直接用弦线代替。","profile":[]}
        raw_points.extend(_entity_points(entity))

    if any(not math.isfinite(v) for p in raw_points for v in p):
        return {"ok":False,"message":"图纸含无效坐标，请检查地形线。","profile":[]}

    unique_points = sorted(set(raw_points), key=lambda item: (item[0], item[1]))
    if len(unique_points) < 2:
        return {"ok": False, "message": "指定图层至少需要两个有效断面点。", "profile": []}
    min_x = unique_points[0][0]
    profile = [
        {"offset": round(x - min_x, 3), "elevation": round(y, 3)}
        for x, y in unique_points
    ]
    return {
        "ok": True,
        "message": f"已提取 {len(profile)} 个断面点，横距原点平移{min_x:g}m。" + ("DXF未声明单位，必须确认坐标以米计。" if declared_units == 0 else "DXF单位为米。"),
        "profile": profile,
        "ground_layer": target_layer,
        'original_x_origin_m': min_x,
        'units':'m',
        'units_confirmed_by_file': declared_units == 6,
    }


def analyze_cad_file(path: str | Path, mode: str = "smart") -> dict[str, Any]:
    # @hook SECTION-CAD-IMPORT-FILE 20260817
    # Reads a local DXF file so users do not need to type layer names by hand.
    file_path = Path(str(path or "")).expanduser()
    if not file_path.exists():
        return {"ok": False, "message": "请选择存在的 CAD/DXF 文件。"}
    extension = file_path.suffix.lower()
    if extension in RECOGNIZED_BUT_UNSUPPORTED_EXTENSIONS:
        return {
            "ok": False,
            "status": "dwg_requires_conversion",
            "file": str(file_path),
            "file_name": file_path.name,
            "extension": extension,
            "message": "当前可直接读取 DXF。DWG 请先在 CAD 中另存为 DXF，后续可接入 ODA 转换器实现自动转换。",
        }
    if extension not in SUPPORTED_CAD_EXTENSIONS:
        return {
            "ok": False,
            "status": "unsupported_format",
            "file": str(file_path),
            "file_name": file_path.name,
            "extension": extension,
            "message": "当前断面导入支持 DXF 文件；其他格式请先转换为 DXF。",
        }

    try:
        entities = _parse_dxf_entities(file_path)
    except OSError as exc:
        return {"ok": False, "file": str(file_path), "message": f"读取 DXF 失败：{exc}"}

    layers = _collect_layers_from_entities(entities)
    layer_names = [item["name"] for item in layers]
    analysis = analyze_layers(layer_names, mode=mode)
    ground_guess = next((item for item in analysis.get("guesses", []) if item.get("role") == "ground_line"), None)
    profile_preview = extract_section_profile(file_path, ground_guess.get("layer") if ground_guess else None)
    analysis.update(
        {
            "file": str(file_path),
            "file_name": file_path.name,
            "extension": extension,
            "entity_count": sum(1 for item in entities if str(item.get("type", "")).upper() in DRAWABLE_ENTITY_TYPES),
            "layer_details": layers,
            "profile_preview": profile_preview,
            "message": f"已读取 {file_path.name}，识别到 {len(layer_names)} 个图层、{sum(1 for item in entities if str(item.get('type', '')).upper() in DRAWABLE_ENTITY_TYPES)} 个可绘制对象。请复核映射后进入计算。",
        }
    )
    return analysis
