from __future__ import annotations

import json
import math
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Tuple

EPS = 1e-12
HARMONIC_TOL = 0.05


@dataclass(frozen=True)
class Reper4:
    U: float
    I: float
    R: float
    D: float


@dataclass(frozen=True)
class LambdaResult:
    lambda_value: Optional[float]
    delta: Optional[float]
    auth: Optional[float]
    status: str
    message: str = ""
    lambda_quadratic: Optional[float] = None
    delta_quadratic: Optional[float] = None
    auth_quadratic: Optional[float] = None
    q_factor: Optional[float] = None
    quadratic_status: str = "not_computed"
    interval_status: str = "not_computed"


@dataclass
class PixNode:
    id: str
    label: str
    type: str
    peak: Dict[str, Any]
    reper: Optional[Reper4]
    payload: Dict[str, Any]


@dataclass
class GraphEdge:
    source: str
    target: str
    type: str
    label: str = ""


@dataclass
class Defect:
    id: str
    defect_class: str
    title: str
    message: str
    severity: float
    pix_ids: List[str]
    payload: Dict[str, Any]


def safe_float(value: Any, default: float = 0.0) -> float:
    if value is None:
        return default
    if isinstance(value, (int, float)):
        return float(value)
    try:
        return float(str(value).replace(",", ".").replace(" ", ""))
    except ValueError:
        return default


def _classify_interval(delta: Optional[float], delta_quadratic: Optional[float], quadratic_status: str) -> str:
    if delta is None:
        return "linear_singular"
    if delta_quadratic is None or quadratic_status != "ok":
        return "quadratic_singular"
    if delta <= HARMONIC_TOL and delta_quadratic <= HARMONIC_TOL:
        return "full_quadratic_harmonic"
    if delta <= HARMONIC_TOL and delta_quadratic > HARMONIC_TOL:
        return "quadratic_obstruction"
    if delta > HARMONIC_TOL and delta_quadratic <= HARMONIC_TOL:
        return "metric_closed_amplitude_gap"
    if delta <= 0.25 and delta_quadratic <= 0.25:
        return "near_harmonic_complex_interval"
    return "open_interval"


def lambda_truth(U: float, I: float, R: float, D: float) -> LambdaResult:
    """Compute KLT lambda truth and the v9.6 quadratic PIX@PEAKS obstruction layer.

    Linear layer:       lambda = cr(U,I;R,D).
    Quadratic layer:    lambda_quadratic = cr(U^2,I^2;R^2,D^2).
    Obstruction factor: Q = lambda_quadratic / lambda = ((U+R)(I+D))/((U+D)(I+R)) when defined.
    """
    numerator = (U - R) * (I - D)
    denominator = (U - D) * (I - R)
    if abs(denominator) < EPS:
        return LambdaResult(None, None, None, "singular", "Projective denominator is zero or too close to zero.")
    value = numerator / denominator
    delta = abs(value + 1.0)
    auth = 1.0 / (1.0 + max(0.0, delta))

    numerator2 = (U * U - R * R) * (I * I - D * D)
    denominator2 = (U * U - D * D) * (I * I - R * R)
    if abs(denominator2) < EPS:
        return LambdaResult(
            value, delta, auth, "ok", "",
            lambda_quadratic=None, delta_quadratic=None, auth_quadratic=None, q_factor=None,
            quadratic_status="singular", interval_status="quadratic_singular",
        )
    value2 = numerator2 / denominator2
    delta2 = abs(value2 + 1.0)
    auth2 = 1.0 / (1.0 + max(0.0, delta2))
    q_den = (U + D) * (I + R)
    q_factor = ((U + R) * (I + D) / q_den) if abs(q_den) >= EPS else None
    interval_status = _classify_interval(delta, delta2, "ok")
    return LambdaResult(
        value, delta, auth, "ok", "",
        lambda_quadratic=value2, delta_quadratic=delta2, auth_quadratic=auth2, q_factor=q_factor,
        quadratic_status="ok", interval_status=interval_status,
    )


def d_star(U: float, I: float, R: float) -> Optional[float]:
    denominator = U + I - 2.0 * R
    if abs(denominator) < EPS:
        return None
    return (2.0 * U * I - U * R - I * R) / denominator


def d_star_quadratic(U: float, I: float, R: float) -> Dict[str, Any]:
    """Quadratic harmonic foundation for cr(U^2,I^2;R^2,D^2)=-1."""
    denominator = U * U + I * I - 2.0 * R * R
    if abs(denominator) < EPS:
        return {"status": "singular", "D2": None, "branches": []}
    d2 = (2.0 * U * U * I * I - R * R * I * I - U * U * R * R) / denominator
    if d2 < 0:
        return {"status": "complex_branch_required", "D2": d2, "branches": []}
    root = math.sqrt(d2)
    return {"status": "ok", "D2": d2, "branches": [root, -root]}


def magnitude_shift_index(cost_a: float, cost_b: float) -> Optional[float]:
    if cost_a <= 0 or cost_b <= 0:
        return None
    return abs(math.log(cost_b / cost_a))


def deficit_index(c_min: float, c_offer: float) -> Optional[float]:
    if c_min <= 0:
        return None
    return max(0.0, (c_min - c_offer) / c_min)


def collapse_risk_index(severities: Iterable[float], deltas: Iterable[float], msi_values: Iterable[float], di_values: Iterable[float]) -> float:
    values: List[float] = []
    values.extend(max(0.0, min(1.0, x)) for x in severities)
    values.extend(max(0.0, min(5.0, x)) / 5.0 for x in deltas)
    values.extend(max(0.0, min(2.0, x)) / 2.0 for x in msi_values)
    values.extend(max(0.0, min(1.0, x)) for x in di_values)
    if not values:
        return 0.0
    return max(0.0, min(1.0, sum(values) / len(values)))


class Klt51Engine:
    def __init__(self) -> None:
        self.nodes: List[PixNode] = []
        self.edges: List[GraphEdge] = []
        self.defects: List[Defect] = []
        self.lambda_results: Dict[str, LambdaResult] = {}
        self.msi_values: List[float] = []
        self.di_values: List[float] = []

    def analyze(self, data: Dict[str, Any]) -> Dict[str, Any]:
        self.nodes = []
        self.edges = []
        self.defects = []
        self.lambda_results = {}
        self.msi_values = []
        self.di_values = []

        for resource in data.get("resources", []):
            self.nodes.append(PixNode(str(resource.get("id")), str(resource.get("name", resource.get("id"))), "resource", {
                "linked": True, "resourced": True, "justified": True, "physically_feasible": True
            }, None, dict(resource)))

        for delivery in data.get("deliveries", []):
            basis = str(delivery.get("document_basis", delivery.get("basis", "")))
            node_id = str(delivery.get("id"))
            self.nodes.append(PixNode(node_id, str(delivery.get("name", node_id)), "delivery", {
                "linked": True, "scheduled": True, "justified": bool(basis), "physically_feasible": True
            }, None, dict(delivery)))
            if not basis:
                self._add_defect("logistics", "Delivery without basis", f"Delivery {node_id} has no document basis.", 0.45, [node_id])

        for work in data.get("works", []):
            node_id = str(work.get("id"))
            reper = self._reper(work.get("reper"))
            resources = [str(x) for x in work.get("resources", [])]
            predecessors = [str(x) for x in work.get("predecessors", [])]
            deliveries = [str(x) for x in work.get("deliveries", [])]
            peak = {
                "linked": bool(predecessors) or safe_float(work.get("start_day", work.get("startDay", 0))) <= 1,
                "scheduled": True,
                "resourced": bool(resources),
                "justified": reper is not None,
                "physically_feasible": safe_float(work.get("end_day", work.get("endDay", 0))) >= safe_float(work.get("start_day", work.get("startDay", 0))),
            }
            self.nodes.append(PixNode(node_id, str(work.get("name", node_id)), "work", peak, reper, dict(work)))
            for pred in predecessors:
                self.edges.append(GraphEdge(pred, node_id, "precedes", "precedes"))
            for res in resources:
                self.edges.append(GraphEdge(node_id, res, "uses_resource", "uses"))
            for delivery in deliveries:
                self.edges.append(GraphEdge(node_id, delivery, "requires_delivery", "requires"))

        for cost in data.get("costs", []):
            node_id = str(cost.get("id"))
            basis = str(cost.get("basis", ""))
            reper = self._reper(cost.get("reper"))
            self.nodes.append(PixNode(node_id, str(cost.get("name", node_id)), "cost", {
                "linked": bool(cost.get("parent_id") or cost.get("parentId")),
                "priced": True,
                "justified": bool(basis),
                "physically_feasible": True,
            }, reper, dict(cost)))
            parent = str(cost.get("parent_id", cost.get("parentId", "")))
            if parent:
                self.edges.append(GraphEdge(node_id, parent, "cost_rollup", "rollup"))
            coefficient = safe_float(cost.get("coefficient"), 1.0)
            if coefficient > 1.0 and not basis:
                self._add_defect("cost", "Coefficient without basis", f"Cost {node_id} has coefficient {coefficient} without basis.", 0.55, [node_id])

        for finance in data.get("finance", []):
            node_id = str(finance.get("id", f"fin_{len(self.nodes) + 1}"))
            planned = safe_float(finance.get("planned"))
            actual = safe_float(finance.get("actual"))
            amount = max(abs(planned), abs(actual), 1.0)
            deviation = abs(actual - planned) / max(abs(planned), 1.0) if planned else 0.5
            confirmation = 0.85 if finance.get("document_ref") else 0.55
            reper = Reper4(
                max(0, min(1, planned / amount)),
                max(0, min(1, 1 - deviation)),
                max(0, min(1, actual / amount)),
                confirmation,
            )
            self.nodes.append(PixNode(node_id, str(finance.get("description", node_id)), "finance", {
                "linked": True, "priced": True, "justified": confirmation > 0.6, "physically_feasible": True
            }, reper, dict(finance)))
            if deviation > 0.3:
                self._add_defect("finance", "Finance deviation", f"Finance {node_id} deviates by {deviation * 100:.1f}%.", min(1, deviation), [node_id])

        self._compute_lambda()
        self._check_work_predecessors(data.get("works", []))
        self._check_resource_conflicts(data.get("works", []))
        self._check_cost_scale(data.get("costs", []))
        self._check_deficit(data)

        cri = collapse_risk_index(
            [d.severity for d in self.defects],
            [x for r in self.lambda_results.values() for x in (r.delta, r.delta_quadratic) if x is not None],
            self.msi_values,
            self.di_values,
        )
        return {
            "project_id": str(data.get("project_id", data.get("projectId", "KLT51"))),
            "title": str(data.get("title", "KLT 5.1 project")),
            "pix_count": len(self.nodes),
            "edge_count": len(self.edges),
            "defect_count": len(self.defects),
            "lambda_count": len(self.lambda_results),
            "delta_avg": self._delta_avg(),
            "delta_quadratic_avg": self._delta_quadratic_avg(),
            "quadratic_obstruction_count": sum(1 for r in self.lambda_results.values() if r.interval_status == "quadratic_obstruction"),
            "msi_max": max(self.msi_values) if self.msi_values else 0.0,
            "di_max": max(self.di_values) if self.di_values else 0.0,
            "cri": cri,
            "nodes": [self._node_to_json(n) for n in self.nodes],
            "edges": [asdict(e) for e in self.edges],
            "defects": [asdict(d) for d in self.defects],
            "lambda": {k: asdict(v) for k, v in self.lambda_results.items()},
        }

    def _node_to_json(self, node: PixNode) -> Dict[str, Any]:
        d = asdict(node)
        if node.reper is not None:
            d["reper"] = asdict(node.reper)
        return d

    def _add_defect(self, cls: str, title: str, message: str, severity: float, pix_ids: List[str], payload: Optional[Dict[str, Any]] = None) -> None:
        self.defects.append(Defect(f"DEF-{len(self.defects) + 1:04d}", cls, title, message, max(0, min(1, severity)), pix_ids, payload or {}))

    def _reper(self, value: Any) -> Optional[Reper4]:
        if not isinstance(value, dict):
            return None
        return Reper4(safe_float(value.get("U", value.get("u"))), safe_float(value.get("I", value.get("i"))), safe_float(value.get("R", value.get("r"))), safe_float(value.get("D", value.get("d"))))

    def _compute_lambda(self) -> None:
        for node in self.nodes:
            if node.reper is None:
                continue
            result = lambda_truth(node.reper.U, node.reper.I, node.reper.R, node.reper.D)
            self.lambda_results[node.id] = result
            if result.status == "singular":
                self._add_defect("lambda", "Singular Reper", f"Cannot compute lambda for {node.label}.", 0.75, [node.id])
            elif result.delta is not None and result.delta > 1.0:
                self._add_defect("lambda", "High lambda defect", f"{node.label} has delta={result.delta:.3f}.", min(1.0, result.delta / 5.0), [node.id], asdict(result))
            if result.quadratic_status == "singular":
                self._add_defect("quadratic_lambda", "Quadratic singularity", f"{node.label} has singular quadratic PIX@PEAKS interval.", 0.70, [node.id], asdict(result))
            elif result.interval_status == "quadratic_obstruction":
                self._add_defect("quadratic_lambda", "Quadratic obstruction", f"{node.label} is lambda-harmonic but not quadratically harmonic.", 0.65, [node.id], asdict(result))
            elif result.delta_quadratic is not None and result.delta_quadratic > 1.0:
                self._add_defect("quadratic_lambda", "High quadratic lambda defect", f"{node.label} has delta2={result.delta_quadratic:.3f}.", min(1.0, result.delta_quadratic / 5.0), [node.id], asdict(result))

    def _check_work_predecessors(self, works: List[Dict[str, Any]]) -> None:
        by_id = {str(w.get("id")): w for w in works}
        for work in works:
            work_id = str(work.get("id"))
            start = safe_float(work.get("start_day", work.get("startDay")))
            for pred_id in work.get("predecessors", []):
                pred = by_id.get(str(pred_id))
                if pred is None:
                    self._add_defect("technology", "Missing predecessor", f"Work {work_id} references missing predecessor {pred_id}.", 0.85, [work_id, str(pred_id)])
                    continue
                if start <= safe_float(pred.get("end_day", pred.get("endDay"))):
                    self._add_defect("technology", "Technology overlap", f"Work {work_id} starts before predecessor {pred_id} is complete.", 0.65, [work_id, str(pred_id)])

    def _check_resource_conflicts(self, works: List[Dict[str, Any]]) -> None:
        for idx, a in enumerate(works):
            for b in works[idx + 1:]:
                shared = set(map(str, a.get("resources", []))).intersection(map(str, b.get("resources", [])))
                if not shared:
                    continue
                a_start = safe_float(a.get("start_day", a.get("startDay")))
                a_end = safe_float(a.get("end_day", a.get("endDay")))
                b_start = safe_float(b.get("start_day", b.get("startDay")))
                b_end = safe_float(b.get("end_day", b.get("endDay")))
                if a_start <= b_end and b_start <= a_end:
                    self._add_defect("resource", "Resource conflict", f"Works {a.get('id')} and {b.get('id')} share {', '.join(sorted(shared))} with overlapping dates.", 0.60, [str(a.get("id")), str(b.get("id"))])

    def _check_cost_scale(self, costs: List[Dict[str, Any]]) -> None:
        by_id = {str(c.get("id")): c for c in costs}
        children: Dict[str, List[Dict[str, Any]]] = {}
        for c in costs:
            parent = str(c.get("parent_id", c.get("parentId", "")))
            if parent:
                children.setdefault(parent, []).append(c)
        for parent, items in children.items():
            child_sum = sum(safe_float(i.get("amount")) for i in items)
            parent_amount = safe_float(by_id.get(parent, {}).get("amount"))
            msi = magnitude_shift_index(max(child_sum, 1), max(parent_amount, 1))
            if msi is not None:
                self.msi_values.append(msi)
                if msi > 0.5:
                    self._add_defect("cost", "Cost scale jump", f"Cost parent {parent} differs from child sum; MSI={msi:.3f}.", min(1, msi / 2), [parent])

    def _check_deficit(self, data: Dict[str, Any]) -> None:
        c_min = safe_float(data.get("c_min", data.get("cMin")))
        c_offer = safe_float(data.get("c_offer", data.get("cOffer")))
        if c_min > 0 and c_offer > 0:
            di = deficit_index(c_min, c_offer)
            if di is not None:
                self.di_values.append(di)
                if di > 0.05:
                    self._add_defect("cost", "Deficit zone", f"Offer is lower than minimum control cost by {di * 100:.1f}%.", min(1, di * 3), ["project"])

    def _delta_avg(self) -> float:
        values = [r.delta for r in self.lambda_results.values() if r.delta is not None]
        return sum(values) / len(values) if values else 0.0

    def _delta_quadratic_avg(self) -> float:
        values = [r.delta_quadratic for r in self.lambda_results.values() if r.delta_quadratic is not None]
        return sum(values) / len(values) if values else 0.0


def generate_report(result: Dict[str, Any]) -> str:
    lines = [
        "# KLT 5.1 Lambda Report",
        "",
        f"Project: {result['title']}",
        f"PIX: {result['pix_count']}",
        f"Edges: {result['edge_count']}",
        f"Defects: {result['defect_count']}",
        f"CRI: {result['cri']:.4f}",
        f"delta avg: {result['delta_avg']:.4f}",
        f"delta quadratic avg: {result.get('delta_quadratic_avg', 0.0):.4f}",
        f"quadratic obstructions: {result.get('quadratic_obstruction_count', 0)}",
        f"MSI max: {result['msi_max']:.4f}",
        f"DI max: {result['di_max']:.4f}",
        "",
        "## Defects",
    ]
    for defect in result["defects"]:
        lines.append(f"- {defect['id']} [{defect['defect_class']}] {defect['title']}: {defect['message']} severity={defect['severity']:.2f}")
    lines.append("")
    lines.append("## Legal note")
    lines.append("KLT 5.1 output is an analytical indicator and does not replace official expert, engineering, audit, legal or investment conclusions.")
    return "\n".join(lines)


def generate_scheme_svg(result: Dict[str, Any], width: int = 1000, height: int = 640) -> str:
    nodes = result["nodes"]
    edges = result["edges"]
    positions: Dict[str, Tuple[float, float]] = {}
    for idx, node in enumerate(nodes):
        angle = 2 * math.pi * idx / max(1, len(nodes))
        positions[node["id"]] = (width / 2 + math.cos(angle) * width * 0.36, height / 2 + math.sin(angle) * height * 0.34)
    defects_by_pix: Dict[str, float] = {}
    for defect in result["defects"]:
        for pix_id in defect["pix_ids"]:
            defects_by_pix[pix_id] = max(defects_by_pix.get(pix_id, 0), defect["severity"])
    s = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">', '<rect width="100%" height="100%" fill="#F8FAFC"/>']
    s.append('<text x="24" y="38" font-family="Arial" font-size="24" fill="#14324B">KLT 5.1 PIX@PEAKS scheme</text>')
    for edge in edges:
        a = positions.get(edge["source"])
        b = positions.get(edge["target"])
        if a and b:
            s.append(f'<line x1="{a[0]:.1f}" y1="{a[1]:.1f}" x2="{b[0]:.1f}" y2="{b[1]:.1f}" stroke="#64748B" stroke-width="1.5"/>')
    for node in nodes:
        x, y = positions[node["id"]]
        sev = defects_by_pix.get(node["id"], 0)
        color = "#FCA5A5" if sev > 0.66 else ("#FDE68A" if sev > 0.33 else "#BAE6FD")
        label = str(node["label"]).replace("&", "&amp;").replace("<", "&lt;")[:18]
        s.append(f'<rect x="{x-58:.1f}" y="{y-24:.1f}" width="116" height="48" rx="10" fill="{color}" stroke="#0F172A"/>')
        s.append(f'<text x="{x:.1f}" y="{y:.1f}" text-anchor="middle" font-family="Arial" font-size="12" fill="#0F172A">{label}</text>')
    s.append('</svg>')
    return "\n".join(s)


def generate_blueprint_svg(result: Dict[str, Any], width: int = 1000, height: int = 600) -> str:
    works = [n for n in result["nodes"] if n["type"] == "work"]
    zones = sorted({str(w["payload"].get("zone", "zone")) for w in works}) or ["zone"]
    lane_h = (height - 90) / len(zones)
    max_end = max([safe_float(w["payload"].get("end_day", 1)) for w in works] or [1])
    s = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">', '<rect width="100%" height="100%" fill="white"/>']
    s.append('<text x="24" y="38" font-family="Arial" font-size="24" fill="#14324B">KLT 5.1 schematic drawing</text>')
    for zi, zone in enumerate(zones):
        y = 70 + zi * lane_h
        s.append(f'<rect x="24" y="{y:.1f}" width="{width-48}" height="{lane_h-10:.1f}" fill="#F8FAFC" stroke="#CBD5E1"/>')
        s.append(f'<text x="34" y="{y+22:.1f}" font-family="Arial" font-size="14">{zone}</text>')
    for w in works:
        zone = str(w["payload"].get("zone", "zone"))
        zi = zones.index(zone)
        start = safe_float(w["payload"].get("start_day"))
        end = safe_float(w["payload"].get("end_day"))
        x = 170 + (start / max_end) * (width - 230)
        rw = max(50, ((end - start + 1) / max_end) * (width - 230))
        y = 94 + zi * lane_h
        label = str(w["label"]).replace("&", "&amp;")[:22]
        s.append(f'<rect x="{x:.1f}" y="{y:.1f}" width="{rw:.1f}" height="40" fill="#A7F3D0" stroke="#0F172A" rx="6"/>')
        s.append(f'<text x="{x+8:.1f}" y="{y+24:.1f}" font-family="Arial" font-size="12">{label}</text>')
    s.append('</svg>')
    return "\n".join(s)


def generate_chart_svg(result: Dict[str, Any], width: int = 1000, height: int = 540) -> str:
    metrics = {
        "CRI": result["cri"],
        "delta": min(1.0, result["delta_avg"] / 3.0),
        "MSI": min(1.0, result["msi_max"] / 2.0),
        "DI": min(1.0, result["di_max"]),
        "DEF": min(1.0, result["defect_count"] / max(1, result["pix_count"])),
    }
    s = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">', '<rect width="100%" height="100%" fill="white"/>']
    s.append('<text x="24" y="38" font-family="Arial" font-size="24" fill="#14324B">KLT 5.1 risk graph</text>')
    bar_w = (width - 160) / len(metrics)
    for idx, (label, value) in enumerate(metrics.items()):
        x = 80 + idx * bar_w
        h = value * (height - 150)
        y = height - 70 - h
        color = "#F87171" if value > 0.66 else ("#FBBF24" if value > 0.33 else "#34D399")
        s.append(f'<rect x="{x:.1f}" y="{y:.1f}" width="{bar_w*0.62:.1f}" height="{h:.1f}" fill="{color}" rx="8"/>')
        s.append(f'<text x="{x+bar_w*0.31:.1f}" y="{height-40}" text-anchor="middle" font-family="Arial" font-size="14">{label}</text>')
        s.append(f'<text x="{x+bar_w*0.31:.1f}" y="{y-8:.1f}" text-anchor="middle" font-family="Arial" font-size="14">{value:.2f}</text>')
    s.append('</svg>')
    return "\n".join(s)


def analyze_file(path: str) -> Dict[str, Any]:
    data = json.loads(Path(path).read_text(encoding="utf-8"))
    return Klt51Engine().analyze(data)
