"""Conflict resolution for sync engine.

Strategies:
  - LWW (Last Writer Wins): Default for most tables. The row with
    the higher _sync_version wins.
  - Merge: For observations and patterns, merge specific fields
    (e.g., sum confirmations, keep higher confidence).
  - Tombstone: Deletes propagate — if either side deleted, delete wins.

Merge Rules (per-field):
  Observations:
    - confidence (float): keep higher value (reward confirmation over decay)
    - confirmations (int): take max (monotonically increasing counter)
    - pruned (bool): pruned=True wins (explicit human action, irreversible)
    - knowledge_id (FK): keep non-null; if both non-null, LWW
    - pattern_id (FK): keep non-null; if both non-null, LWW
    - stability_days (float): keep higher value (more stable = more confirmed)
    - last_confirmed_at (timestamp): keep later timestamp
    - all other fields: LWW

  Patterns:
    - confirmations (int): take max
    - applications (int): take max
    - confidence (enum): keep higher level (low < medium < high < verified)
    - all other fields: LWW
"""
from __future__ import annotations

import logging
from datetime import datetime, timezone
from enum import Enum
from typing import Any

log = logging.getLogger(__name__)


class ConflictStrategy(str, Enum):
    """How to resolve conflicting changes."""
    LWW = "lww"           # Last Writer Wins (by sync_version)
    MERGE = "merge"       # Field-level merge
    TOMBSTONE = "tombstone"  # Delete always wins


# Default strategy per table
TABLE_STRATEGIES: dict[str, ConflictStrategy] = {
    "patterns": ConflictStrategy.MERGE,
    "pattern_evidence": ConflictStrategy.LWW,
    "knowledge_entries": ConflictStrategy.LWW,
    "work_items": ConflictStrategy.LWW,
    "sessions": ConflictStrategy.LWW,
    "observations": ConflictStrategy.MERGE,
    "documents": ConflictStrategy.LWW,
}

# Conflict audit log — list of dicts recording every resolution.
# Callers can drain this for persistence (see flush_audit_log).
_audit_log: list[dict[str, Any]] = []


def resolve_conflict(
    table: str,
    local_row: dict[str, Any] | None,
    remote_row: dict[str, Any] | None,
    *,
    strategy: ConflictStrategy | None = None,
) -> dict[str, Any] | None:
    """Resolve a conflict between local and remote versions of a row.

    Args:
        table: Table name.
        local_row: Current local version (None if deleted locally).
        remote_row: Incoming remote version (None if deleted remotely).
        strategy: Override the default strategy for this table.

    Returns:
        The winning row dict, or None if the row should be deleted.
    """
    if strategy is None:
        strategy = TABLE_STRATEGIES.get(table, ConflictStrategy.LWW)

    # One side is None (deleted)
    if local_row is None and remote_row is None:
        return None
    if local_row is None:
        return remote_row
    if remote_row is None:
        if strategy == ConflictStrategy.TOMBSTONE:
            return None  # Delete wins
        # LWW/MERGE: remote deleted but local exists — keep local
        return local_row

    # Both sides exist — this is a real conflict. Resolve and audit.
    merged_fields: list[str] = []
    if strategy == ConflictStrategy.LWW:
        result = _resolve_lww(local_row, remote_row)
    elif strategy == ConflictStrategy.MERGE:
        result, merged_fields = _resolve_merge(table, local_row, remote_row)
    elif strategy == ConflictStrategy.TOMBSTONE:
        result = _resolve_lww(local_row, remote_row)
    else:
        result = _resolve_lww(local_row, remote_row)

    # Audit every conflict where both sides existed
    row_id = local_row.get("id", remote_row.get("id", "?"))
    _record_audit(table, row_id, strategy, local_row, remote_row, result, merged_fields)

    return result


def _record_audit(
    table: str,
    row_id: str,
    strategy: ConflictStrategy,
    local: dict[str, Any],
    remote: dict[str, Any],
    resolved: dict[str, Any] | None,
    merged_fields: list[str] | None = None,
) -> None:
    """Append a conflict resolution record to the in-memory audit log."""
    entry = {
        "table": table,
        "row_id": row_id,
        "strategy": strategy.value,
        "local_version": local.get("_sync_version", 0),
        "remote_version": remote.get("_sync_version", 0),
        "local_node": local.get("_node_id", ""),
        "remote_node": remote.get("_node_id", ""),
        "resolved_to": "delete" if resolved is None else (
            "local" if resolved.get("_node_id") == local.get("_node_id") else "remote"
        ),
        "merged_fields": merged_fields or [],
        "timestamp": datetime.now(timezone.utc).isoformat(),
    }
    _audit_log.append(entry)
    log.debug("conflict resolved: %s/%s → %s", table, row_id, entry["resolved_to"])


def flush_audit_log() -> list[dict[str, Any]]:
    """Drain the in-memory audit log and return all entries.

    Call this after each sync cycle to persist audit records.
    """
    entries = list(_audit_log)
    _audit_log.clear()
    return entries


def persist_audit_log(db, entries: list[dict[str, Any]] | None = None) -> int:
    """Write audit log entries to the _conflict_audit table.

    Args:
        db: Database with _conflict_audit table.
        entries: Entries to persist. If None, flushes the in-memory log.

    Returns:
        Number of entries persisted.
    """
    if entries is None:
        entries = flush_audit_log()
    if not entries:
        return 0

    for e in entries:
        db.execute(
            "INSERT INTO _conflict_audit "
            "(table_name, row_id, strategy, local_version, remote_version, "
            " local_node, remote_node, resolved_to, merged_fields, created_at) "
            "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            (
                e["table"], e["row_id"], e["strategy"],
                e["local_version"], e["remote_version"],
                e["local_node"], e["remote_node"],
                e["resolved_to"], ",".join(e.get("merged_fields", [])),
                e["timestamp"],
            ),
        )
    return len(entries)


def _resolve_lww(
    local_row: dict[str, Any], remote_row: dict[str, Any]
) -> dict[str, Any]:
    """Last Writer Wins: prefer HLC timestamp, fall back to _sync_version.

    Resolution order:
      1. If both rows have _hlc_timestamp, compare those (causally consistent)
      2. Otherwise, compare _sync_version (monotonic per-node counter)
      3. Tiebreaker: node_id lexicographic (deterministic)
    """
    # Try HLC first (causally consistent across nodes)
    local_hlc = local_row.get("_hlc_timestamp")
    remote_hlc = remote_row.get("_hlc_timestamp")

    if local_hlc and remote_hlc:
        from greymatter_core.sync.hlc import HLCTimestamp
        try:
            l_ts = HLCTimestamp.from_str(str(local_hlc))
            r_ts = HLCTimestamp.from_str(str(remote_hlc))
            if r_ts > l_ts:
                return remote_row
            elif l_ts > r_ts:
                return local_row
            # HLC tie — fall through to sync_version
        except (ValueError, TypeError):
            pass  # Malformed HLC — fall back to sync_version

    # Fall back to sync_version (per-node monotonic counter)
    local_version = local_row.get("_sync_version", 0) or 0
    remote_version = remote_row.get("_sync_version", 0) or 0

    if remote_version > local_version:
        return remote_row
    elif local_version > remote_version:
        return local_row
    else:
        # Tiebreaker: compare node_id lexicographically (deterministic)
        local_node = local_row.get("_node_id", "")
        remote_node = remote_row.get("_node_id", "")
        return remote_row if remote_node > local_node else local_row


def _resolve_merge(
    table: str, local_row: dict[str, Any], remote_row: dict[str, Any]
) -> tuple[dict[str, Any], list[str]]:
    """Merge strategy: combine fields from both versions.

    Returns (resolved_row, list_of_merged_field_names).
    """
    if table == "patterns":
        return _merge_pattern(local_row, remote_row)
    elif table == "observations":
        return _merge_observation(local_row, remote_row)
    else:
        return _resolve_lww(local_row, remote_row), []


def _merge_pattern(
    local: dict[str, Any], remote: dict[str, Any]
) -> tuple[dict[str, Any], list[str]]:
    """Merge two pattern rows.

    Merge rules:
      - confirmations (int): take max (monotonic counter)
      - applications (int): take max (monotonic counter)
      - confidence (enum str): keep higher level (low < medium < high < verified)
      - all other fields: LWW by _sync_version

    Returns (resolved_row, list_of_merged_field_names).
    """
    winner = _resolve_lww(local, remote)
    result = dict(winner)
    merged = []

    # Merge numeric counters: take the max from either side
    for field in ("confirmations", "applications"):
        local_val = local.get(field, 0) or 0
        remote_val = remote.get(field, 0) or 0
        best = max(local_val, remote_val)
        if best != result.get(field):
            merged.append(field)
        result[field] = best

    # Merge confidence: keep the higher level
    confidence_order = {"low": 0, "medium": 1, "high": 2, "verified": 3}
    local_conf = confidence_order.get(str(local.get("confidence", "low")), 0)
    remote_conf = confidence_order.get(str(remote.get("confidence", "low")), 0)
    if local_conf >= remote_conf:
        best_conf = local.get("confidence", "low")
    else:
        best_conf = remote.get("confidence", "low")
    if best_conf != result.get("confidence"):
        merged.append("confidence")
    result["confidence"] = best_conf

    return result, merged


def _merge_observation(
    local: dict[str, Any], remote: dict[str, Any]
) -> tuple[dict[str, Any], list[str]]:
    """Merge two observation rows with per-field rules.

    Merge rules:
      - confidence (float): keep higher value (reward confirmation over decay)
      - confirmations (int): take max (monotonic counter)
      - pruned (bool/int): True wins (explicit human action, irreversible)
      - knowledge_id (FK): keep non-null; if both non-null, LWW
      - pattern_id (FK): keep non-null; if both non-null, LWW
      - stability_days (float): keep higher value (more stable)
      - last_confirmed_at (str): keep later timestamp
      - all other fields: LWW by _sync_version

    Returns (resolved_row, list_of_merged_field_names).
    """
    winner = _resolve_lww(local, remote)
    result = dict(winner)
    merged = []

    # 1. confidence (float): higher wins — confirmation > decay
    local_conf = float(local.get("confidence", 0) or 0)
    remote_conf = float(remote.get("confidence", 0) or 0)
    best_conf = max(local_conf, remote_conf)
    if best_conf != result.get("confidence"):
        merged.append("confidence")
    result["confidence"] = best_conf

    # 2. confirmations (int): max — monotonic counter (only if column exists)
    if "confirmations" in local or "confirmations" in remote:
        local_confirms = int(local.get("confirmations", 0) or 0)
        remote_confirms = int(remote.get("confirmations", 0) or 0)
        best_confirms = max(local_confirms, remote_confirms)
        if best_confirms != result.get("confirmations"):
            merged.append("confirmations")
        result["confirmations"] = best_confirms

    # 3. pruned (bool): True wins — pruning is an irreversible human action
    local_pruned = bool(local.get("pruned", 0))
    remote_pruned = bool(remote.get("pruned", 0))
    if local_pruned or remote_pruned:
        if not result.get("pruned"):
            merged.append("pruned")
        result["pruned"] = 1

    # 4. knowledge_id (FK): prefer non-null; if both set, LWW
    local_kid = local.get("knowledge_id")
    remote_kid = remote.get("knowledge_id")
    if local_kid and remote_kid:
        pass  # LWW already picked the winner
    elif local_kid and not remote_kid:
        if result.get("knowledge_id") != local_kid:
            merged.append("knowledge_id")
        result["knowledge_id"] = local_kid
    elif remote_kid and not local_kid:
        if result.get("knowledge_id") != remote_kid:
            merged.append("knowledge_id")
        result["knowledge_id"] = remote_kid

    # 5. pattern_id (FK): same logic as knowledge_id
    local_pid = local.get("pattern_id")
    remote_pid = remote.get("pattern_id")
    if local_pid and remote_pid:
        pass  # LWW
    elif local_pid and not remote_pid:
        if result.get("pattern_id") != local_pid:
            merged.append("pattern_id")
        result["pattern_id"] = local_pid
    elif remote_pid and not local_pid:
        if result.get("pattern_id") != remote_pid:
            merged.append("pattern_id")
        result["pattern_id"] = remote_pid

    # 6. stability_days (float): higher = more confirmed
    if "stability_days" in local or "stability_days" in remote:
        local_stab = float(local.get("stability_days", 0) or 0)
        remote_stab = float(remote.get("stability_days", 0) or 0)
        best_stab = max(local_stab, remote_stab)
        if best_stab != float(result.get("stability_days", 0) or 0):
            merged.append("stability_days")
        result["stability_days"] = best_stab

    # 7. last_confirmed_at (timestamp): keep the later one
    local_lca = str(local.get("last_confirmed_at", "") or "")
    remote_lca = str(remote.get("last_confirmed_at", "") or "")
    if local_lca and remote_lca:
        best_lca = max(local_lca, remote_lca)  # ISO strings compare correctly
    else:
        best_lca = local_lca or remote_lca
    if best_lca and best_lca != result.get("last_confirmed_at"):
        merged.append("last_confirmed_at")
    if best_lca:
        result["last_confirmed_at"] = best_lca

    return result, merged
