import os
from collections import defaultdict
from opendbc.car.tests.car_diff import format_diff, format_numeric_diffs
from openpilot.selfdrive.test.process_replay.compare_logs import compare_logs
from openpilot.selfdrive.test.process_replay.process_replay import PROC_REPLAY_DIR


class MsgWrap:
  """Adapter so to_dict() includes defaults"""
  def __init__(self, msg):
    self._msg = msg
  def to_dict(self) -> dict:
    return self._msg.to_dict(verbose=True)


def diff_process(cfg, ref_msgs, new_msgs) -> tuple | None:
  ref = defaultdict(list)
  new = defaultdict(list)
  for m in ref_msgs:
    if m.which() in cfg.subs:
      ref[m.which()].append(m)
  for m in new_msgs:
    if m.which() in cfg.subs:
      new[m.which()].append(m)

  diffs = []
  for sub in cfg.subs:
    if len(ref[sub]) != len(new[sub]):
      diffs.append((f"{sub} (message count)", 0, (len(ref[sub]), len(new[sub])), 0))
    for i, (r, n) in enumerate(zip(ref[sub], new[sub], strict=False)):
      for d in compare_logs([r], [n], cfg.ignore, tolerance=cfg.tolerance):
        if d[0] == "change":
          a, b = d[2]
          if a != a and b != b:
            continue
          diffs.append((d[1], i, d[2], r.logMonoTime))
        elif d[0] in ("add", "remove"):
          for item in d[2]:
            if item[1] != item[1]:
              continue
            diffs.append((f"{d[1]}.{item[0]}", i, (d[0], item[1]), r.logMonoTime))
  return (diffs, ref, new) if diffs else None


def diff_format(diffs, ref, new, field) -> list[str]:
  if any(part.isdigit() for part in field.split(".")):
    return format_numeric_diffs(diffs)
  msg_type = field.split(".")[0]
  ref_ts = [(m.logMonoTime, MsgWrap(m)) for m in ref.get(msg_type, [])]
  new_wrapped = [MsgWrap(m) for m in new.get(msg_type, [])]
  if not ref_ts or not new_wrapped:
    return format_numeric_diffs(diffs)
  return format_diff(diffs, ref_ts, new_wrapped, field)


def diff_report(replay_diffs, segments) -> None:
  seg_to_plat = {seg: plat for plat, seg in segments}

  with_diffs, errors, n_passed = [], [], 0
  for seg, proc, data in replay_diffs:
    plat = seg_to_plat.get(seg, "UNKNOWN")
    if data is None:
      n_passed += 1
    elif isinstance(data, str):
      errors.append((plat, seg, proc, data))
    else:
      with_diffs.append((plat, seg, proc, data))

  icon = "⚠️" if with_diffs else "✅"
  lines = [
    "## Process replay diff report",
    "Replays driving segments through this PR and compares the behavior to master.",
    "Please review any changes carefully to ensure they are expected.\n",
    f"{icon}  {len(with_diffs)} changed, {n_passed} passed, {len(errors)} errors",
  ]

  for plat, seg, proc, err in errors:
    lines.append(f"\nERROR {plat} - {seg} [{proc}]: {err}")

  if with_diffs:
    lines.append("<details><summary><b>Show changes</b></summary>\n\n```")
    for plat, seg, proc, (diffs, ref, new) in with_diffs:
      lines.append(f"\n{plat} - {seg} [{proc}]")
      by_field = defaultdict(list)
      for d in diffs:
        by_field[d[0]].append(d)
      for field, fd in sorted(by_field.items()):
        lines.append(f"\n  {field} ({len(fd)} diffs)")
        lines.extend(diff_format(fd, ref, new, field))
    lines.append("```\n</details>")

  with open(os.path.join(PROC_REPLAY_DIR, "diff_report.txt"), "w") as f:
    f.write("\n".join(lines))
