"""Lossless calculation snapshots and portable engineering charts (standard library only)."""
from __future__ import annotations

from html import escape
from dataclasses import asdict, is_dataclass
import json
import math
from pathlib import Path
from typing import Any
from .engineering_labels import LABELS


def display_label(key: str) -> str:
    common = {'name': '项目', 'value': '数值', 'unit': '单位', 'note': '说明',
              'source': '资料来源', 'Fr': '弗汝德数 Fr'}
    return common.get(key, LABELS.get(key, LABELS.get(key.replace(' ', '_'), key)))


def table_matrix(rows: list, columns: list | None = None, column_labels: list | None = None) -> list[list]:
    """One ordered cell model for export and clipboard-compatible result sheets."""
    if not rows:
        return []
    columns = columns or list(dict.fromkeys(k for row in rows if isinstance(row, dict) for k in row))
    if not columns:
        columns = [f'第{i+1}列' for i in range(max(len(r) if isinstance(r, (tuple, list)) else 1 for r in rows))]
    labels = column_labels if column_labels and len(column_labels) == len(columns) else [display_label(k) for k in columns]
    matrix = [labels]
    for row in rows:
        values = [row.get(k) for k in columns] if isinstance(row, dict) else list(row) if isinstance(row, (tuple, list)) else [row]
        matrix.append((values + [None] * len(columns))[:len(columns)])
    return matrix


def label_result_tables(result: dict, metadata: dict | None = None) -> dict:
    metadata = metadata or {}
    for table in result.get('tables', []):
        if table.get('columns'):
            table.setdefault('column_labels', [display_label(c) for c in table['columns']])
        for row in table.get('rows', []):
            if isinstance(row, dict) and isinstance(row.get('name'), str):
                key = row['name']
                row['name'] = metadata.get(key, {}).get('label') or display_label(key)
    return result


def number(value: Any) -> float | None:
    if value is None or value == "" or isinstance(value, bool):
        return None
    try:
        result = float(value)
        return result if math.isfinite(result) else None
    except (TypeError, ValueError):
        return None


def validate_finite(value: Any, path: str = "参数") -> None:
    if is_dataclass(value) and not isinstance(value, type):
        validate_finite(asdict(value), path)
        return
    if isinstance(value, float) and not math.isfinite(value):
        raise ValueError(f"{path} 不是有限数值，请检查输入和计算范围。")
    if isinstance(value, dict):
        for key, child in value.items():
            validate_finite(child, f"{path}.{key}")
    elif isinstance(value, (tuple, list)):
        for i, child in enumerate(value):
            validate_finite(child, f"{path}[{i+1}]")


def cell(value: Any) -> str:
    if value is None:
        return "—"
    if isinstance(value, (dict, list, tuple)):
        return json.dumps(value, ensure_ascii=False)
    return str(value)


def table_text(title: str, rows: list, columns: list | None = None, column_labels: list | None = None) -> str:
    if not rows:
        return ""
    matrix = table_matrix(rows, columns, column_labels)
    lines = [title, '| ' + ' | '.join(matrix[0]) + ' |', '| ' + ' | '.join('---' for _ in matrix[0]) + ' |']
    for values in matrix[1:]:
        lines.append('| ' + ' | '.join(cell(v).replace('|', '/').replace('\n', '；') for v in values) + ' |')
    return '\n'.join(lines)


def charts(result: dict) -> list[dict]:
    specs = []
    for series in result.get('series', []):
        xkey, ykey = series.get('xKey', 'time_h'), series.get('yKey', 'discharge_m3s')
        specs.append({'title':series.get('title', '成果曲线'), 'xLabel':series.get('xLabel', xkey), 'yLabel':series.get('yLabel', series.get('unit', ykey)), 'points':[(number(p.get(xkey)), number(p.get(ykey))) for p in series.get('points', [])]})
    for key, xkey, ykey, title, xlabel, ylabel in [
        ('rating_curve', 'discharge_m3s', 'water_level_m', '水位流量关系', '流量 Q（m³/s）', '水位 Z（m）'),
        ('profile_curve', 'distance_m', 'water_level_m', '沿程水面线', '里程（m）', '水位 Z（m）'),
        ('profile_curve', 'distance_m', 'energy_grade_m', '沿程能量线', '里程（m）', '能量高程（m）'),
        ('profile_curve', 'distance_m', 'bed_elevation_m', '沿程河床线', '里程（m）', '河床高程（m）'),
        ('curve_points', 'distance_m', 'water_level_m', '沿程变化曲线', '距离（m）', '水位 / 壅水（m）'),
    ]:
        if result.get(key):
            data = result.get('chart_rating_curve', result[key]) if key == 'rating_curve' else result[key]
            spec = {'title':title, 'xLabel':xlabel, 'yLabel':ylabel, 'points':[(number(p.get(xkey)), number(p.get(ykey, p.get('backwater_m')))) for p in data]}
            if key == 'rating_curve':
                markers = result.get('rating_markers') or ([result['rating_marker']] if result.get('rating_marker') else [])
                spec['markers'] = [{'x':number(m.get(xkey)), 'y':number(m.get(ykey)), 'label':m.get('label', '设计水位')} for m in markers
                                   if number(m.get(xkey)) is not None and number(m.get(ykey)) is not None]
            specs.append(spec)
    profiles = result.get('section_profiles') or ([result['section_profile']] if result.get('section_profile') else [])
    for i, profile in enumerate(profiles):
        points = []
        for p in profile.get('points', []):
            if isinstance(p, dict):
                points.append((number(p.get('x', p.get('distance_m', p.get('offset')))), number(p.get('z', p.get('elevation_m', p.get('elevation'))))))
            else:
                points.append((number(p[0]), number(p[1])))
        spec = {'title':f"{profile.get('name', i+1)} 断面", 'xLabel':'横距（m）', 'yLabel':'高程（m）', 'points':points}
        level = number(profile.get('water_level_m', profile.get('water_level')))
        if level is not None and points:
            xs = [x for x, y in points if x is not None]
            if xs:
                spec['waterline'] = [(min(xs), level), (max(xs), level)]
        specs.append(spec)
    return [s for s in specs if any(x is not None and y is not None for x, y in s['points'])]


def svg_chart(spec: dict) -> str:
    points = spec['points']
    markers = spec.get('markers', [])
    good = [(x,y) for x,y in points + spec.get('waterline', []) + [(m['x'],m['y']) for m in markers] if x is not None and y is not None]
    xmin, xmax = min(x for x,y in good), max(x for x,y in good)
    ymin, ymax = min(y for x,y in good), max(y for x,y in good)
    dx, dy = max(xmax-xmin, 1e-6), max(ymax-ymin, 1e-6)
    ymin, ymax = ymin-dy*.08, ymax+dy*.08
    def xy(x,y):
        return 95+(x-xmin)/dx*760, 348-(y-ymin)/(ymax-ymin)*262
    def line(data, color):
        commands, pen = [], False
        for x,y in data:
            if x is None or y is None:
                pen = False
                continue
            px,py = xy(x,y)
            commands.append(f"{'L' if pen else 'M'}{px:.3f},{py:.3f}")
            pen = True
        return f'<path d="{" ".join(commands)}" fill="none" stroke="{color}" stroke-width="2.5"/>'
    parts = ['<svg xmlns="http://www.w3.org/2000/svg" width="960" height="430" viewBox="0 0 960 430">', '<rect width="960" height="430" fill="white"/>', '<g font-family="Times New Roman, KaiTi, 楷体, serif" fill="#173b53">', f'<text x="480" y="30" text-anchor="middle" font-size="19">{escape(spec["title"])}</text>', f'<text x="95" y="62" font-size="14">{escape(spec["yLabel"])}</text>']
    for i in range(6):
        x=xmin+dx*i/5
        y=ymin+(ymax-ymin)*i/5
        px,_=xy(x,y)
        _,py=xy(x,y)
        parts.extend([f'<path d="M95,{py} H855 M{px},86 V348" stroke="#dce6ec" fill="none"/>', f'<text x="86" y="{py+4}" text-anchor="end" font-size="12">{y:.6g}</text>', f'<text x="{px}" y="370" text-anchor="middle" font-size="12">{x:.6g}</text>'])
    parts.extend([line(points, '#1565c0'), line(spec.get('waterline', []), '#00838f')])
    # Numbered guides remain uncluttered even for nearly coincident design levels.
    for i, marker in enumerate(markers):
        px, py = xy(marker['x'], marker['y'])
        parts.extend([f'<path d="M95,{py} H{px} V348" stroke="#b42335" stroke-dasharray="5 4" fill="none"/>',
                      f'<circle cx="{px}" cy="{py}" r="4" fill="#b42335"/>',
                      f'<text x="{min(px+7,835)}" y="{max(py-7,96)}" font-size="12" fill="#b42335">{i+1}</text>'])
    parts.append(f'<text x="480" y="411" text-anchor="middle" font-size="14">{escape(spec["xLabel"])}</text>')
    if markers:
        height = 448 + 24 * len(markers)
        parts[0] = f'<svg xmlns="http://www.w3.org/2000/svg" width="960" height="{height}" viewBox="0 0 960 {height}">'
        parts[1] = f'<rect width="960" height="{height}" fill="white"/>'
        for i, marker in enumerate(markers):
            label = f"{i+1}. {marker['label']}  Q={marker['x']:.3f} m³/s  Z={marker['y']:.3f} m"
            parts.append(f'<text x="95" y="{444+24*i}" font-size="14">{escape(label)}</text>')
    parts.append('</g></svg>')
    return ''.join(parts)


def snapshot_text(program, result: dict) -> str:
    parts = [program.name, '一、公式来源与适用方法', result.get('source') or program.source,
             '二、本次输入参数', table_text('', result.get('parameter_snapshot', result.get('input_rows', []))),
             '三、计算公式与过程', result.get('calculation_book_text') or result.get('formula') or '当前计算器未返回过程文本，需进一步核验。']
    for t in result.get('tables', []):
        parts.append(table_text(t.get('title', '过程表'), t.get('rows', []), t.get('columns'), t.get('column_labels')))
    parts.extend(['四、最终结果', table_text('', result.get('metrics', [])), '五、成果曲线与完整坐标'])
    for spec in charts(result):
        parts.append(table_text(spec['title'], [list(p) for p in spec['points']], [spec['xLabel'],spec['yLabel']]))
    parts.extend(['六、复核说明', '\n'.join(result.get('diag') or result.get('diagnostics', [])) or '计算执行完成不等于项目成果已审定；请核对资料适用范围与参数依据。',
                  '七、成果记录', f"计算时间：{result.get('calculated_at', '未记录')}；参数指纹：{result.get('parameter_hash', '未记录')}"])
    return '\n\n'.join(str(p) for p in parts if p)


def export_snapshot_xlsx(content: str, path: Path, result: dict, title: str) -> None:
    from .export import _build_calculation_sheets, _write_audit_workbook, _xlsx_range_ref
    sheets = []
    for table in result.get('tables', []):
        matrix = table_matrix(table.get('rows', []), table.get('columns'), table.get('column_labels'))
        if matrix:
            rows = [([table.get('title', '成果表')], 2), (matrix[0], 1)]
            rows.extend((row, 4) for row in matrix[1:])
            sheets.append({'name':table.get('title', '成果表'), 'rows':rows, 'charts':[]})
    sheets.extend({'name':name, 'rows':rows, 'charts':[]} for name, rows in _build_calculation_sheets(content, title))
    for i,spec in enumerate(charts(result), 1):
        name = f'曲线{i:02d}'
        rows = [[spec['title']], [spec['xLabel'],spec['yLabel']], *[list(p) for p in spec['points']]]
        series=[{'name':spec['title'], 'x_ref':_xlsx_range_ref(name,3,1,len(rows),1), 'y_ref':_xlsx_range_ref(name,3,2,len(rows),2), 'color':'1565C0'}]
        if spec.get('waterline'):
            rows[1].extend(['水位线横距（m）','设计水位（m）'])
            for j, point in enumerate(spec['waterline'],2):
                while len(rows) <= j:
                    rows.append([None,None])
                rows[j].extend(point)
            series.append({'name':'设计水位线', 'x_ref':_xlsx_range_ref(name,3,3,4,3), 'y_ref':_xlsx_range_ref(name,3,4,4,4), 'color':'00838F'})
        for marker in spec.get('markers', []):
            start_col = len(rows[1]) + 1
            label = f"{marker['label']} Q={marker['x']:.3f} m³/s Z={marker['y']:.3f} m"
            rows[1].extend([label + ' 流量', label + ' 水位'])
            rows[2] += [None] * (start_col - 1 - len(rows[2]))
            rows[2].extend([marker['x'], marker['y']])
            series.append({'name':label, 'x_ref':_xlsx_range_ref(name,3,start_col,3,start_col),
                           'y_ref':_xlsx_range_ref(name,3,start_col+1,3,start_col+1), 'color':'B42335', 'show_label': True})
        sheets.append({'name':name, 'rows':[(row, 2 if j == 0 else 1 if j == 1 else 4) for j,row in enumerate(rows)], 'charts':[{'title':spec['title'], 'x_axis':spec['xLabel'], 'y_axis':spec['yLabel'], 'series':series}]})
    _write_audit_workbook(path, sheets)
