from __future__ import annotations

from datetime import datetime
from pathlib import Path
from typing import Any
import json
import math

from .font_policy import font_runs


class CadSectionExportError(ValueError):
    """Raised when a result does not contain enough section data for CAD export."""


def _num(value: Any, default: float | None = None) -> float | None:
    if value is None or value == "":
        return default
    try:
        number = float(value)
    except (TypeError, ValueError):
        return default
    if not math.isfinite(number):
        return default
    return number


def _sanitize_text(value: Any) -> str:
    text = str(value or "").strip()
    return text.replace("\r", " ").replace("\n", " ") or "未命名断面"


def _sanitize_filename(value: str) -> str:
    forbidden = '\\/:*?"<>|'
    cleaned = "".join("_" if ch in forbidden else ch for ch in value).strip(" .")
    return cleaned or "断面水位图"


def parse_points(raw: Any) -> list[tuple[float, float]]:
    if raw is None:
        return []
    if isinstance(raw, str):
        text = raw.strip()
        if not text:
            return []
        if text[0] in "[{":
            try:
                return parse_points(json.loads(text))
            except json.JSONDecodeError:
                pass
        points: list[tuple[float, float]] = []
        for part in text.replace("|", ";").replace("，", ",").split(";"):
            item = part.strip()
            if not item:
                continue
            if "," not in item:
                continue
            x_raw, z_raw = item.split(",", 1)
            x = _num(x_raw)
            z = _num(z_raw)
            if x is not None and z is not None:
                points.append((x, z))
        return points
    if isinstance(raw, dict):
        if "points" in raw:
            return parse_points(raw.get("points"))
        x = _coordinate(raw, ("x", "distance", "station", "offset"))
        z = _coordinate(raw, ("z", "elevation", "elevation_m"))
        return [(x, z)] if x is not None and z is not None else []
    if isinstance(raw, (list, tuple)):
        points: list[tuple[float, float]] = []
        for item in raw:
            if isinstance(item, dict):
                x = _coordinate(item, ("x", "distance", "station", "offset"))
                z = _coordinate(item, ("z", "elevation", "elevation_m"))
            elif isinstance(item, (list, tuple)) and len(item) >= 2:
                x = _num(item[0])
                z = _num(item[1])
            else:
                x = z = None
            if x is not None and z is not None:
                points.append((x, z))
        return points
    return []


def _coordinate(point: dict[str, Any], keys: tuple[str, ...]) -> float | None:
    # Zero station and zero elevation are legitimate survey coordinates.
    return next((_num(point[key]) for key in keys if point.get(key) not in (None, "")), None)


def _first_section_from_sections_json(raw: Any, use_last: bool = True) -> dict[str, Any] | None:
    if not raw:
        return None
    try:
        rows = json.loads(raw) if isinstance(raw, str) else raw
    except json.JSONDecodeError:
        return None
    if not isinstance(rows, list) or not rows:
        return None
    section = rows[-1] if use_last else rows[0]
    return section if isinstance(section, dict) else None


def _profile_from_result(result: dict[str, Any]) -> dict[str, Any] | None:
    for key in ("section_profile", "cad_section", "section"):
        profile = result.get(key)
        if isinstance(profile, dict) and parse_points(profile.get("points")):
            return profile
    profiles = result.get("section_profiles")
    if isinstance(profiles, list) and profiles:
        for profile in reversed(profiles):
            if isinstance(profile, dict) and parse_points(profile.get("points")):
                return profile
    points = parse_points(result.get("points"))
    if points:
        return {"name": result.get("section_name") or result.get("title"), "points": points}
    return None


def _profile_from_params(params: dict[str, Any]) -> dict[str, Any] | None:
    points = parse_points(params.get("points") or params.get("section_points"))
    if points:
        return {"name": params.get("name") or params.get("section_name") or "设计断面", "points": points}
    section = _first_section_from_sections_json(params.get("sections_json"))
    if section:
        return {
            "name": section.get("name") or "设计断面",
            "points": parse_points(section.get("points")),
            "roughness_n": section.get("roughness_n"),
        }
    return None


def _water_level(result: dict[str, Any], profile: dict[str, Any], params: dict[str, Any]) -> float | None:
    for source in (profile, result, params):
        for key in (
            "water_level",
            "water_level_m",
            "design_water_level",
            "design_water_level_m",
            "stage",
            "z",
            "Z",
        ):
            number = _num(source.get(key) if isinstance(source, dict) else None)
            if number is not None:
                return number
    return None


def extract_section_profile(result: dict[str, Any] | None, params: dict[str, Any] | None = None) -> dict[str, Any]:
    result = result or {}
    params = params or {}
    profile = _profile_from_result(result) or _profile_from_params(params)
    if not profile:
        raise CadSectionExportError("未找到断面点列，无法导出CAD断面。请先完成水位/断面计算，或输入断面点列。")
    points = parse_points(profile.get("points"))
    if len(points) < 2:
        raise CadSectionExportError("断面点列不足，至少需要2个地形点。")
    water_level = _water_level(result, profile, params)
    if water_level is None and result.get("manning_mode") != "curve":
        raise CadSectionExportError("未找到计算水位，无法标注水位线。")
    thalweg_x = _num(profile.get("thalweg_x") or profile.get("thalweg_x_m"))
    thalweg_z = _num(profile.get("thalweg_z") or profile.get("thalweg_elevation") or profile.get("thalweg_elevation_m"))
    if thalweg_x is None or thalweg_z is None:
        thalweg_x, thalweg_z = min(points, key=lambda item: item[1])
    return {
        "name": _sanitize_text(profile.get("name") or result.get("title") or params.get("name") or "设计断面"),
        "points": points,
        "water_level": float(water_level) if water_level is not None else None,
        "thalweg_x": float(thalweg_x),
        "thalweg_z": float(thalweg_z),
    }


def _pair(code: int, value: Any) -> str:
    return f"{code}\n{value}"


def _line(layer: str, x1: float, y1: float, x2: float, y2: float) -> list[str]:
    return [
        _pair(0, "LINE"),
        _pair(8, layer),
        _pair(10, f"{x1:.3f}"),
        _pair(20, f"{y1:.3f}"),
        _pair(30, "0"),
        _pair(11, f"{x2:.3f}"),
        _pair(21, f"{y2:.3f}"),
        _pair(31, "0"),
    ]


def _text(layer: str, x: float, y: float, height: float, text: str) -> list[str]:
    # MTEXT font runs preserve one editable label without estimating glyph widths.
    content = "".join(
        "{\\f" + ("KaiTi" if family == "楷体" else family) + "|b0|i0;"
        + value.replace("\\", "\\\\").replace("{", "\\{").replace("}", "\\}") + "}"
        for family, value in font_runs(text)
    )
    chunks = [content[index:index + 240] for index in range(0, len(content), 240)] or [""]
    return [
        _pair(0, "MTEXT"),
        _pair(100, "AcDbEntity"),
        _pair(8, layer),
        _pair(100, "AcDbMText"),
        _pair(10, f"{x:.3f}"),
        _pair(20, f"{y:.3f}"),
        _pair(30, "0"),
        _pair(40, f"{height:.3f}"),
        _pair(41, 0),
        _pair(71, 7),
        _pair(7, "HTZD_KAITI"),
        *[_pair(3, chunk) for chunk in chunks[:-1]],
        _pair(1, chunks[-1]),
    ]


def _circle(layer: str, x: float, y: float, radius: float) -> list[str]:
    return [
        _pair(0, "CIRCLE"),
        _pair(8, layer),
        _pair(10, f"{x:.3f}"),
        _pair(20, f"{y:.3f}"),
        _pair(30, "0"),
        _pair(40, f"{radius:.3f}"),
    ]


def _polyline(layer: str, points: list[tuple[float, float]]) -> list[str]:
    lines = [_pair(0, "POLYLINE"), _pair(8, layer), _pair(66, 1), _pair(70, 0)]
    for x, y in points:
        lines.extend([_pair(0, "VERTEX"), _pair(8, layer), _pair(10, f"{x:.3f}"), _pair(20, f"{y:.3f}"), _pair(30, "0")])
    lines.extend([_pair(0, "SEQEND"), _pair(8, layer)])
    return lines


def _layer_table() -> list[str]:
    layers = [
        ("HTZD_SECTION_GROUND", 7),
        ("HTZD_SECTION_WATER_LEVEL", 5),
        ("HTZD_SECTION_THALWEG", 1),
        ("HTZD_SECTION_TEXT", 2),
    ]
    rows = [
        _pair(0, "SECTION"),
        _pair(2, "TABLES"),
        _pair(0, "TABLE"),
        _pair(2, "LAYER"),
        _pair(70, len(layers)),
    ]
    for name, color in layers:
        rows.extend([_pair(0, "LAYER"), _pair(2, name), _pair(70, 0), _pair(62, color), _pair(6, "CONTINUOUS")])
    rows.extend([
        _pair(0, "ENDTAB"), _pair(0, "TABLE"), _pair(2, "STYLE"), _pair(70, 1),
        _pair(0, "STYLE"), _pair(2, "HTZD_KAITI"), _pair(70, 0),
        _pair(40, 0), _pair(41, 1), _pair(50, 0), _pair(71, 0), _pair(42, 2.5),
        _pair(3, "simkai.ttf"), _pair(4, ""),
        _pair(1001, "ACAD"), _pair(1000, "KaiTi"), _pair(1071, 34),
        _pair(0, "ENDTAB"), _pair(0, "TABLE"), _pair(2, "APPID"), _pair(70, 1),
        _pair(0, "APPID"), _pair(2, "ACAD"), _pair(70, 0),
        _pair(0, "ENDTAB"), _pair(0, "ENDSEC"),
    ])
    return rows


def build_dxf(profile: dict[str, Any], program_id: str = "") -> str:
    points = list(profile["points"])
    water_level = _num(profile.get("water_level"))
    thalweg_x = float(profile["thalweg_x"])
    thalweg_z = float(profile["thalweg_z"])
    min_x = min(x for x, _ in points)
    max_x = max(x for x, _ in points)
    min_z = min(z for _, z in points)
    max_z = max(z for _, z in points)
    if water_level is not None:
        max_z = max(max_z, water_level)
    width = max(max_x - min_x, 1.0)
    height = max(max_z - min_z, 1.0)
    text_h = max(width, height) * 0.025
    text_h = min(max(text_h, 0.35), 2.5)
    mark_r = text_h * 0.7
    label_x = min_x + width * 0.03
    title_y = max_z + height * 0.12 + text_h

    rows: list[str] = [
        _pair(0, "SECTION"),
        _pair(2, "HEADER"),
        _pair(9, "$ACADVER"),
        _pair(1, "AC1021"),
        _pair(9, "$DWGCODEPAGE"),
        _pair(3, "ANSI_936"),
        _pair(9, "$INSUNITS"),
        _pair(70, 6),
        _pair(0, "ENDSEC"),
    ]
    rows.extend(_layer_table())
    rows.extend([_pair(0, "SECTION"), _pair(2, "ENTITIES")])
    drawing_title = "断面水位图" if water_level is not None else "断面地形图"
    rows.extend(_text("HTZD_SECTION_TEXT", label_x, title_y, text_h * 1.3, f"{profile['name']} {drawing_title}"))
    rows.extend(_text("HTZD_SECTION_TEXT", label_x, title_y - text_h * 1.8, text_h, f"程序：{program_id or '水位断面计算'}"))
    rows.extend(_polyline("HTZD_SECTION_GROUND", points))
    if water_level is not None:
        rows.extend(_line("HTZD_SECTION_WATER_LEVEL", min_x, water_level, max_x, water_level))
        rows.extend(_text("HTZD_SECTION_TEXT", label_x, water_level + text_h * 0.8, text_h, f"设计水位 Z={water_level:.2f} m"))
    rows.extend(_circle("HTZD_SECTION_THALWEG", thalweg_x, thalweg_z, mark_r))
    rows.extend(_line("HTZD_SECTION_THALWEG", thalweg_x - mark_r * 1.4, thalweg_z, thalweg_x + mark_r * 1.4, thalweg_z))
    rows.extend(_line("HTZD_SECTION_THALWEG", thalweg_x, thalweg_z - mark_r * 1.4, thalweg_x, thalweg_z + mark_r * 1.4))
    rows.extend(_text("HTZD_SECTION_TEXT", thalweg_x + mark_r * 2.0, thalweg_z - text_h * 1.3, text_h, f"深泓点 x={thalweg_x:.2f} m, z={thalweg_z:.2f} m"))
    rows.extend([_pair(0, "ENDSEC"), _pair(0, "EOF")])
    return "\n".join(rows) + "\n"


def export_section_dxf(
    program_id: str,
    result: dict[str, Any] | None = None,
    params: dict[str, Any] | None = None,
    target_path: str | Path | None = None,
) -> dict[str, Any]:
    # @hook CAD-SECTION-EXPORT 20260817
    # Exports calculated section profile, water level and thalweg marker to a CAD DXF file.
    try:
        profile = extract_section_profile(result, params)
        if target_path:
            path = Path(target_path)
        else:
            stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            filename = _sanitize_filename(f"{program_id}_{profile['name']}_断面水位图_{stamp}.dxf")
            path = Path("D:/workspace/cad_exports") / filename
        if path.suffix.lower() != ".dxf":
            path = path.with_suffix(".dxf")
        profiles = (result or {}).get('section_profiles') or []
        outputs = [(path, profile)]
        if len(profiles) > 1:
            parsed = [extract_section_profile({'section_profile': p}, params) for p in profiles]
            outputs = [(path if i == 0 else path.with_name(f'{path.stem}_{i+1:02d}_{_sanitize_filename(p["name"])}.dxf'), p) for i,p in enumerate(parsed)]
            profile = parsed[0]
        drawings = [(target, build_dxf(item, program_id=program_id)) for target,item in outputs]
        path.parent.mkdir(parents=True, exist_ok=True)
        for target, drawing in drawings:
            target.write_text(drawing, encoding="utf-8")
    except CadSectionExportError as exc:
        return {"ok": False, "status": "missing_section_data", "message": str(exc)}
    except Exception as exc:  # noqa: BLE001
        return {"ok": False, "status": "cad_export_error", "message": str(exc)}
    return {
        "ok": True,
        "status": "exported",
        "path": str(path),
        "paths": [str(target) for target, _ in drawings],
        "section_count": len(drawings),
        "section_name": profile["name"],
        "water_level": profile["water_level"],
        "thalweg_x": profile["thalweg_x"],
        "thalweg_z": profile["thalweg_z"],
    }
