from __future__ import annotations

import hashlib
import json
from collections import defaultdict
from collections.abc import Mapping
from pathlib import Path
from typing import Any

from ..formatting import h16
from .memory import MemoryMap
from .peripherals.x24164 import (
    X24164_FACTORY_DEFAULT_BYTES,
    X24164_LOGICAL_PAGE_COUNT,
    X24164_LOGICAL_PAGE_SIZE,
    X24164_LOGICAL_SIZE,
    factory_default_words_from_rom,
)


JsonObject = dict[str, Any]

SELECTOR_MAP_BASE = 0xC564
SELECTOR_MAP_COUNT = 0x0200
RECORD_BYTES = 8
SHADOW_BASE = 0xF400


def build_eeprom_snapshot(
    memory: MemoryMap,
    *,
    rom_bytes: bytes | None = None,
    include_image_hex: bool = False,
) -> JsonObject:
    image = memory.dump_eeprom_image()
    selectors_by_offset = _selectors_by_offset(rom_bytes)
    factory_image = _factory_image(rom_bytes)
    write_events = _write_events(memory, selectors_by_offset)
    write_words = _coalesced_write_words(memory, selectors_by_offset)
    factory_diffs = _factory_diffs(image, factory_image, selectors_by_offset)

    report: JsonObject = {
        "kind": "emulator_eeprom_snapshot",
        "summary": {
            "logical_size": len(image),
            "logical_size_hex": f"0x{len(image):04X}",
            "sha256": hashlib.sha256(image).hexdigest(),
            "write_byte_count": len(write_events),
            "write_word_count": len(write_words),
            "factory_diff_word_count": len(factory_diffs),
            "record_count": X24164_LOGICAL_PAGE_COUNT,
        },
        "records": _records(image),
        "write_events": write_events,
        "write_word_events": write_words,
        "factory_diffs": factory_diffs,
        "shadow_f400": _shadow_summary(memory, rom_bytes, selectors_by_offset),
    }
    if include_image_hex:
        report["image_hex"] = image.hex()
    return report


def format_eeprom_snapshot(report: Mapping[str, Any], *, limit: int = 80) -> str:
    summary = report["summary"]
    lines = [
        "Emulator EEPROM Snapshot",
        "",
        f"size={summary['logical_size_hex']} sha256={summary['sha256']}",
        (
            f"writes: bytes={summary['write_byte_count']} words={summary['write_word_count']} "
            f"factory_diff_words={summary['factory_diff_word_count']}"
        ),
        "",
        "Persistent Records:",
    ]
    for record in report.get("records", []):
        lines.append(
            f"- page {record['page_hex']} EEPROM {record['range_hex']} "
            f"bytes={record['bytes_hex']} text={record['ascii']!r}"
        )

    lines.extend(["", "EEPROM Word Writes:"])
    word_events = list(report.get("write_word_events", []))
    if not word_events:
        lines.append("- none since EEPROM setup/load")
    for event in word_events[:limit]:
        suffix = _event_suffix(event)
        lines.append(
            f"- {event['address_hex']} page={event['page_hex']} offset={event['offset_hex']} "
            f"{event['old_word_hex']}->{event['new_word_hex']} source={event['source']}{suffix}"
        )
    if len(word_events) > limit:
        lines.append(f"- ... {len(word_events) - limit} more word writes omitted")

    lines.extend(["", "Factory Diffs:"])
    diffs = list(report.get("factory_diffs", []))
    if not diffs:
        lines.append("- current EEPROM image matches ROM factory/default image")
    for diff in diffs[:limit]:
        suffix = _event_suffix(diff)
        lines.append(
            f"- {diff['address_hex']} page={diff['page_hex']} offset={diff['offset_hex']} "
            f"expected={diff['expected_word_hex']} actual={diff['actual_word_hex']}{suffix}"
        )
    if len(diffs) > limit:
        lines.append(f"- ... {len(diffs) - limit} more factory diffs omitted")

    shadow = report.get("shadow_f400", {})
    lines.extend(["", "F400 Shadow Diffs:"])
    shadow_diffs = list(shadow.get("diffs", [])) if isinstance(shadow, Mapping) else []
    if not shadow_diffs:
        lines.append("- F400-F4FF shadow matches ROM factory words or no ROM factory baseline was supplied")
    for diff in shadow_diffs[:limit]:
        suffix = _event_suffix(diff)
        lines.append(
            f"- {diff['address_hex']} offset={diff['offset_hex']} "
            f"expected={diff['expected_word_hex']} actual={diff['actual_word_hex']}{suffix}"
        )
    if len(shadow_diffs) > limit:
        lines.append(f"- ... {len(shadow_diffs) - limit} more shadow diffs omitted")
    return "\n".join(lines).rstrip() + "\n"


def write_eeprom_snapshot(
    memory: MemoryMap,
    output_path: Path,
    *,
    rom_bytes: bytes | None = None,
    as_json: bool = False,
    include_image_hex: bool = False,
) -> JsonObject:
    report = build_eeprom_snapshot(memory, rom_bytes=rom_bytes, include_image_hex=include_image_hex)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    if as_json:
        output_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
    else:
        output_path.write_text(format_eeprom_snapshot(report), encoding="utf-8")
    return report


def _write_events(memory: MemoryMap, selectors_by_offset: Mapping[int, list[int]]) -> list[JsonObject]:
    events = []
    for index, event in enumerate(memory.p9_bus.x24164_bus.write_events):
        item = _address_info(event.logical_address, selectors_by_offset)
        item.update(
            {
                "index": index,
                "device": event.device,
                "device_offset": event.device_offset,
                "device_offset_hex": f"0x{event.device_offset:03X}",
                "old_value": event.old_value,
                "old_value_hex": f"0x{event.old_value & 0xFF:02X}",
                "new_value": event.new_value,
                "new_value_hex": f"0x{event.new_value & 0xFF:02X}",
                "source": event.source,
            }
        )
        events.append(item)
    return events


def _coalesced_write_words(memory: MemoryMap, selectors_by_offset: Mapping[int, list[int]]) -> list[JsonObject]:
    events = memory.p9_bus.x24164_bus.write_events
    words: list[JsonObject] = []
    index = 0
    while index < len(events):
        event = events[index]
        next_event = events[index + 1] if index + 1 < len(events) else None
        if (
            next_event is not None
            and (event.logical_address & 1) == 0
            and next_event.logical_address == ((event.logical_address + 1) & 0x0FFF)
            and next_event.device == event.device
            and next_event.source == event.source
        ):
            old_word = ((event.old_value & 0xFF) << 8) | (next_event.old_value & 0xFF)
            new_word = ((event.new_value & 0xFF) << 8) | (next_event.new_value & 0xFF)
            item = _address_info(event.logical_address, selectors_by_offset)
            item.update(
                {
                    "index": index,
                    "device": event.device,
                    "old_word": old_word,
                    "old_word_hex": f"0x{old_word:04X}",
                    "new_word": new_word,
                    "new_word_hex": f"0x{new_word:04X}",
                    "source": event.source,
                }
            )
            words.append(item)
            index += 2
        else:
            index += 1
    return words


def _factory_diffs(
    image: bytes,
    factory_image: bytes | None,
    selectors_by_offset: Mapping[int, list[int]],
) -> list[JsonObject]:
    if factory_image is None:
        return []
    diffs = []
    for address in range(0, min(len(image), len(factory_image)), 2):
        expected = (factory_image[address] << 8) | factory_image[address + 1]
        actual = (image[address] << 8) | image[address + 1]
        if expected == actual:
            continue
        item = _address_info(address, selectors_by_offset)
        item.update(
            {
                "expected_word": expected,
                "expected_word_hex": f"0x{expected:04X}",
                "actual_word": actual,
                "actual_word_hex": f"0x{actual:04X}",
            }
        )
        diffs.append(item)
    return diffs


def _shadow_summary(
    memory: MemoryMap,
    rom_bytes: bytes | None,
    selectors_by_offset: Mapping[int, list[int]],
) -> JsonObject:
    if rom_bytes is None:
        return {"diffs": [], "note": "no ROM factory baseline supplied"}
    factory_words = dict(factory_default_words_from_rom(rom_bytes))
    diffs = []
    for offset in range(0, X24164_FACTORY_DEFAULT_BYTES, 2):
        expected = factory_words[offset]
        address = SHADOW_BASE + offset
        high = memory.external.get(address & 0xFFFF, 0xFF)
        low = memory.external.get((address + 1) & 0xFFFF, 0xFF)
        actual = ((high & 0xFF) << 8) | (low & 0xFF)
        if expected == actual:
            continue
        item = _address_info(offset, selectors_by_offset)
        item.update(
            {
                "address": address,
                "address_hex": h16(address),
                "expected_word": expected,
                "expected_word_hex": f"0x{expected:04X}",
                "actual_word": actual,
                "actual_word_hex": f"0x{actual:04X}",
            }
        )
        diffs.append(item)
    return {"diffs": diffs, "diff_count": len(diffs)}


def _records(image: bytes) -> list[JsonObject]:
    records = []
    for page in range(X24164_LOGICAL_PAGE_COUNT):
        base = page * X24164_LOGICAL_PAGE_SIZE
        data = image[base : base + RECORD_BYTES]
        records.append(
            {
                "page": page,
                "page_hex": f"0x{page:X}",
                "address": base,
                "address_hex": f"0x{base:03X}",
                "range_hex": f"0x{base:03X}-0x{base + RECORD_BYTES - 1:03X}",
                "bytes_hex": data.hex(" ").upper(),
                "words_hex": [
                    f"0x{((data[index] << 8) | data[index + 1]):04X}"
                    for index in range(0, len(data), 2)
                ],
                "ascii": _ascii(data),
                "is_blank_spaces": data == (b" " * RECORD_BYTES),
            }
        )
    return records


def _factory_image(rom_bytes: bytes | None) -> bytes | None:
    if rom_bytes is None:
        return None
    image = bytearray([0xFF] * X24164_LOGICAL_SIZE)
    for offset, word in factory_default_words_from_rom(rom_bytes):
        for page in range(X24164_LOGICAL_PAGE_COUNT):
            address = (page * X24164_LOGICAL_PAGE_SIZE) + offset
            image[address] = (word >> 8) & 0xFF
            image[address + 1] = word & 0xFF
    for page in range(1, X24164_LOGICAL_PAGE_COUNT):
        base = page * X24164_LOGICAL_PAGE_SIZE
        for offset in range(0, RECORD_BYTES, 2):
            image[base + offset] = 0x20
            image[base + offset + 1] = 0x20
    return bytes(image)


def _selectors_by_offset(rom_bytes: bytes | None) -> dict[int, list[int]]:
    if rom_bytes is None:
        return {}
    result: dict[int, list[int]] = defaultdict(list)
    for selector in range(SELECTOR_MAP_COUNT):
        address = SELECTOR_MAP_BASE + selector * 2
        if address + 1 >= len(rom_bytes):
            break
        low = rom_bytes[address + 1]
        if low:
            result[low & 0xFE].append(selector)
    return dict(result)


def _address_info(address: int, selectors_by_offset: Mapping[int, list[int]]) -> JsonObject:
    address &= 0x0FFF
    page = (address // X24164_LOGICAL_PAGE_SIZE) & 0x0F
    offset = address & 0xFF
    aligned_offset = offset & 0xFE
    selectors = selectors_by_offset.get(aligned_offset, [])
    return {
        "address": address,
        "address_hex": f"0x{address:03X}",
        "page": page,
        "page_hex": f"0x{page:X}",
        "offset": offset,
        "offset_hex": f"0x{offset:02X}",
        "aligned_offset": aligned_offset,
        "aligned_offset_hex": f"0x{aligned_offset:02X}",
        "record_byte": offset if offset < RECORD_BYTES else None,
        "role": "record_header_or_label" if offset < RECORD_BYTES else "factory_shadow_offset",
        "mapped_selectors": selectors[:24],
        "mapped_selectors_hex": [f"0x{selector:03X}" for selector in selectors[:24]],
    }


def _event_suffix(event: Mapping[str, Any]) -> str:
    parts = []
    if event.get("role"):
        parts.append(str(event["role"]))
    selectors = event.get("mapped_selectors_hex")
    if isinstance(selectors, list) and selectors:
        parts.append("selectors=" + ",".join(str(item) for item in selectors[:6]))
    return f" ({'; '.join(parts)})" if parts else ""


def _ascii(data: bytes) -> str:
    return "".join(chr(value) if 0x20 <= value <= 0x7E else "." for value in data)


__all__ = [
    "build_eeprom_snapshot",
    "format_eeprom_snapshot",
    "write_eeprom_snapshot",
]
