import 'dart:convert';
import 'dart:math' as math;

import '../models/klt_models.dart';
import 'lambda_engine.dart';

class ProjectEngine {
  KltAnalysis analyzeJson(String jsonText) {
    final Map<String, dynamic> root = jsonDecode(jsonText) as Map<String, dynamic>;
    return analyzeMap(root);
  }

  KltAnalysis analyzeMap(Map<String, dynamic> root) {
    final projectId = (root['project_id'] ?? root['projectId'] ?? 'KLT51-DEMO').toString();
    final title = (root['title'] ?? 'KLT 5.1 project').toString();
    final nodes = <PixNode>[];
    final edges = <GraphEdge>[];
    final defects = <KltDefect>[];
    final lambdaByPix = <String, LambdaResult>{};
    final msiValues = <double>[];
    final diValues = <double>[];

    void addDefect(DefectClass cls, String title, String message, double severity, List<String> pixIds, [Map<String, dynamic> payload = const {}]) {
      defects.add(KltDefect(
        id: 'DEF-${(defects.length + 1).toString().padLeft(4, '0')}',
        defectClass: cls,
        title: title,
        message: message,
        severity: severity.clamp(0.0, 1.0).toDouble(),
        pixIds: pixIds,
        payload: payload,
      ));
    }

    final resources = _list(root['resources']);
    for (final item in resources) {
      final id = _string(item['id'], 'res_${nodes.length + 1}');
      nodes.add(PixNode(
        id: id,
        label: _string(item['name'], id),
        type: PixType.resource,
        peak: const PeakState(linked: true, resourced: true, justified: true, physicallyFeasible: true),
        payload: item,
      ));
    }

    final deliveries = _list(root['deliveries']);
    for (final item in deliveries) {
      final id = _string(item['id'], 'del_${nodes.length + 1}');
      final basis = _string(item['document_basis'] ?? item['basis'], '');
      nodes.add(PixNode(
        id: id,
        label: _string(item['name'], id),
        type: PixType.delivery,
        peak: PeakState(linked: true, scheduled: true, justified: basis.isNotEmpty, physicallyFeasible: true),
        payload: item,
      ));
      if (basis.isEmpty) {
        addDefect(DefectClass.logistics, 'Delivery without basis', 'Delivery $id has no document basis.', 0.45, [id]);
      }
    }

    final works = _list(root['works']);
    for (final item in works) {
      final id = _string(item['id'], 'work_${nodes.length + 1}');
      final reper = item['reper'] is Map<String, dynamic> ? Reper4.fromJson(item['reper'] as Map<String, dynamic>) : null;
      final resources = _strings(item['resources']);
      final predecessors = _strings(item['predecessors']);
      final deliveries = _strings(item['deliveries']);
      nodes.add(PixNode(
        id: id,
        label: _string(item['name'], id),
        type: PixType.work,
        peak: PeakState(
          linked: predecessors.isNotEmpty || _num(item['start_day'] ?? item['startDay']) <= 1,
          scheduled: true,
          resourced: resources.isNotEmpty,
          justified: reper != null,
          physicallyFeasible: _num(item['end_day'] ?? item['endDay']) >= _num(item['start_day'] ?? item['startDay']),
        ),
        reper: reper,
        payload: item,
      ));
      for (final pred in predecessors) {
        edges.add(GraphEdge(from: pred, to: id, type: 'precedes', label: 'precedes'));
      }
      for (final res in resources) {
        edges.add(GraphEdge(from: id, to: res, type: 'uses_resource', label: 'uses'));
      }
      for (final del in deliveries) {
        edges.add(GraphEdge(from: id, to: del, type: 'requires_delivery', label: 'requires'));
      }
    }

    final costs = _list(root['costs']);
    for (final item in costs) {
      final id = _string(item['id'], 'cost_${nodes.length + 1}');
      final reper = item['reper'] is Map<String, dynamic> ? Reper4.fromJson(item['reper'] as Map<String, dynamic>) : null;
      final basis = _string(item['basis'], '');
      nodes.add(PixNode(
        id: id,
        label: _string(item['name'], id),
        type: PixType.cost,
        peak: PeakState(linked: _string(item['parent_id'] ?? item['parentId'], '').isNotEmpty, priced: true, justified: basis.isNotEmpty, physicallyFeasible: true),
        reper: reper,
        payload: item,
      ));
      final parentId = _string(item['parent_id'] ?? item['parentId'], '');
      if (parentId.isNotEmpty) {
        edges.add(GraphEdge(from: id, to: parentId, type: 'cost_rollup', label: 'rollup'));
      }
      final coefficient = _num(item['coefficient']);
      if (coefficient > 1.0 && basis.isEmpty) {
        addDefect(DefectClass.cost, 'Coefficient without basis', 'Cost item $id uses coefficient $coefficient without a basis.', 0.55, [id]);
      }
    }

    final finance = _list(root['finance']);
    for (final item in finance) {
      final id = _string(item['id'], 'fin_${nodes.length + 1}');
      final planned = _num(item['planned']);
      final actual = _num(item['actual']);
      final amount = math.max(math.max(planned.abs(), actual.abs()), 1.0);
      final confirmation = _string(item['document_ref'], '').isNotEmpty ? 0.85 : 0.55;
      final deviation = planned.abs() > 0 ? ((actual - planned).abs() / planned.abs()).clamp(0.0, 1.0).toDouble() : 0.5;
      final reper = Reper4(
        u: (planned / amount).clamp(0.0, 1.0).toDouble(),
        i: (1.0 - deviation).clamp(0.0, 1.0).toDouble(),
        r: (actual / amount).clamp(0.0, 1.0).toDouble(),
        d: confirmation,
      );
      nodes.add(PixNode(
        id: id,
        label: _string(item['description'], id),
        type: PixType.finance,
        peak: PeakState(linked: true, priced: true, justified: confirmation > 0.6, physicallyFeasible: true),
        reper: reper,
        payload: item,
      ));
      if (deviation > 0.3) {
        addDefect(DefectClass.finance, 'Finance deviation', 'Finance item $id deviates from planned value by ${(deviation * 100).toStringAsFixed(1)}%.', deviation, [id]);
      }
    }

    for (final node in nodes) {
      if (node.reper != null) {
        final result = LambdaEngine.lambdaTruth(node.reper!);
        lambdaByPix[node.id] = result;
        if (result.status == 'singular') {
          addDefect(DefectClass.lambdaDefect, 'Singular Reper', 'Cannot compute lambda for ${node.label}.', 0.75, [node.id]);
        } else if ((result.delta ?? 0) > 1.0) {
          addDefect(
            DefectClass.lambdaDefect,
            'High lambda defect',
            '${node.label} has delta=${(result.delta ?? 0).toStringAsFixed(3)}.',
            math.min(1.0, (result.delta ?? 0) / 5.0),
            [node.id],
            result.toJson(),
          );
        }
        if (result.quadraticStatus == 'singular') {
          addDefect(DefectClass.lambdaDefect, 'Quadratic singularity', '${node.label} has singular quadratic PIX@PEAKS interval.', 0.70, [node.id], result.toJson());
        } else if (result.intervalStatus == 'quadratic_obstruction') {
          addDefect(DefectClass.lambdaDefect, 'Quadratic obstruction', '${node.label} is lambda-harmonic but not quadratically harmonic.', 0.65, [node.id], result.toJson());
        } else if ((result.deltaQuadratic ?? 0) > 1.0) {
          addDefect(DefectClass.lambdaDefect, 'High quadratic lambda defect', '${node.label} has delta2=${(result.deltaQuadratic ?? 0).toStringAsFixed(3)}.', math.min(1.0, (result.deltaQuadratic ?? 0) / 5.0), [node.id], result.toJson());
        }
      }
      if (node.peak.completeness < 0.5) {
        addDefect(DefectClass.documentation, 'Incomplete PEAKS state', '${node.label} has low PEAKS completeness.', 0.35, [node.id], node.peak.toJson());
      }
    }

    _checkWorkPredecessors(works, addDefect);
    _checkResourceConflicts(works, addDefect);
    _checkCostScale(costs, msiValues, addDefect);

    final cMin = _num(root['c_min'] ?? root['cMin']);
    final cOffer = _num(root['c_offer'] ?? root['cOffer']);
    if (cMin > 0 && cOffer > 0) {
      final di = LambdaEngine.deficitIndex(cMin, cOffer);
      if (di != null) {
        diValues.add(di);
        if (di > 0.05) {
          addDefect(DefectClass.cost, 'Deficit zone', 'Offer is lower than minimum control cost by ${(di * 100).toStringAsFixed(1)}%.', math.min(1.0, di * 3), ['project']);
        }
      }
    }

    final cri = LambdaEngine.collapseRiskIndex(
      severities: defects.map((d) => d.severity),
      deltas: lambdaByPix.values.expand((r) => [r.delta, r.deltaQuadratic]).whereType<double>(),
      msiValues: msiValues,
      diValues: diValues,
    );

    return KltAnalysis(
      projectId: projectId,
      title: title,
      nodes: nodes,
      edges: edges,
      defects: defects,
      lambdaByPix: lambdaByPix,
      cri: cri,
      msiMax: msiValues.isEmpty ? 0.0 : msiValues.reduce(math.max),
      diMax: diValues.isEmpty ? 0.0 : diValues.reduce(math.max),
      generatedAt: DateTime.now(),
    );
  }

  void _checkWorkPredecessors(List<Map<String, dynamic>> works, void Function(DefectClass, String, String, double, List<String>, [Map<String, dynamic>]) addDefect) {
    final byId = {for (final w in works) _string(w['id'], ''): w};
    for (final work in works) {
      final id = _string(work['id'], '');
      final start = _num(work['start_day'] ?? work['startDay']);
      for (final pred in _strings(work['predecessors'])) {
        final predWork = byId[pred];
        if (predWork == null) {
          addDefect(DefectClass.technology, 'Missing predecessor', 'Work $id references missing predecessor $pred.', 0.85, [id, pred]);
          continue;
        }
        final predEnd = _num(predWork['end_day'] ?? predWork['endDay']);
        if (start <= predEnd) {
          addDefect(DefectClass.technology, 'Technology overlap', 'Work $id starts before predecessor $pred is complete.', 0.65, [id, pred]);
        }
      }
    }
  }

  void _checkResourceConflicts(List<Map<String, dynamic>> works, void Function(DefectClass, String, String, double, List<String>, [Map<String, dynamic>]) addDefect) {
    for (var a = 0; a < works.length; a++) {
      for (var b = a + 1; b < works.length; b++) {
        final wa = works[a];
        final wb = works[b];
        final resourcesA = _strings(wa['resources']).toSet();
        final resourcesB = _strings(wb['resources']).toSet();
        final shared = resourcesA.intersection(resourcesB);
        if (shared.isEmpty) continue;
        final aStart = _num(wa['start_day'] ?? wa['startDay']);
        final aEnd = _num(wa['end_day'] ?? wa['endDay']);
        final bStart = _num(wb['start_day'] ?? wb['startDay']);
        final bEnd = _num(wb['end_day'] ?? wb['endDay']);
        final overlap = aStart <= bEnd && bStart <= aEnd;
        if (overlap) {
          addDefect(DefectClass.resource, 'Resource conflict', 'Works ${wa['id']} and ${wb['id']} use shared resource(s) ${shared.join(', ')} at overlapping dates.', 0.6, [_string(wa['id'], ''), _string(wb['id'], '')]);
        }
      }
    }
  }

  void _checkCostScale(List<Map<String, dynamic>> costs, List<double> msiValues, void Function(DefectClass, String, String, double, List<String>, [Map<String, dynamic>]) addDefect) {
    final byParent = <String, List<Map<String, dynamic>>>{};
    for (final c in costs) {
      final parent = _string(c['parent_id'] ?? c['parentId'], '');
      if (parent.isNotEmpty) byParent.putIfAbsent(parent, () => []).add(c);
    }
    for (final entry in byParent.entries) {
      final parentAmount = _num(costs.firstWhere((c) => _string(c['id'], '') == entry.key, orElse: () => {'amount': 0})['amount']);
      final childSum = entry.value.fold<double>(0, (s, c) => s + _num(c['amount']));
      final msi = LambdaEngine.magnitudeShiftIndex(math.max(childSum, 1), math.max(parentAmount, 1));
      if (msi != null) {
        msiValues.add(msi);
        if (msi > 0.5) {
          addDefect(DefectClass.cost, 'Cost scale jump', 'Cost parent ${entry.key} differs from child sum; MSI=${msi.toStringAsFixed(3)}.', math.min(1.0, msi / 2.0), [entry.key]);
        }
      }
    }
  }
}

List<Map<String, dynamic>> _list(dynamic value) {
  if (value is List) {
    return value.whereType<Map>().map((m) => Map<String, dynamic>.from(m)).toList();
  }
  return <Map<String, dynamic>>[];
}

List<String> _strings(dynamic value) {
  if (value is List) return value.map((v) => v.toString()).toList();
  if (value is String && value.trim().isNotEmpty) return value.split(',').map((v) => v.trim()).toList();
  return <String>[];
}

String _string(dynamic value, String fallback) => value == null ? fallback : value.toString();

double _num(dynamic value) {
  if (value is num) return value.toDouble();
  if (value is String) return double.tryParse(value.replaceAll(',', '.').replaceAll(' ', '')) ?? 0.0;
  return 0.0;
}
