# -*- coding: utf-8 -*-
"""
MCP Config Sync Engine — Merge and synchronize configs across AI clients.

Merge strategy:
  1. Union of all servers across all detected configs
  2. On conflicts (same server name, different config): newest file wins
  3. Dry-run mode: show diff without applying changes
  4. Per-client export: adapt root key and format per client

Merged from products/mcp_config_sync/sync_engine.py (R009) into tools/ (R010).
"""
import copy
import json
import sys
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Tuple

if sys.platform == "win32":
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")

from .detector import DetectedConfig, detect_configs


@dataclass
class SyncPlan:
    """Plan for syncing configs — generated before applying."""
    source_configs: List[DetectedConfig]
    merged_servers: Dict[str, dict]
    changes_per_client: Dict[str, List[str]]  # client_path -> list of changes
    conflicts: List[dict]
    timestamp: str = ""

    def __post_init__(self):
        if not self.timestamp:
            self.timestamp = datetime.now().isoformat()

    @property
    def total_changes(self) -> int:
        return sum(len(v) for v in self.changes_per_client.values())

    @property
    def has_conflicts(self) -> bool:
        return len(self.conflicts) > 0


# ============================================================================
# MERGE LOGIC
# ============================================================================

def _read_config_data(dc: DetectedConfig) -> dict:
    """Read and parse config from a DetectedConfig."""
    text = dc.path.read_text(encoding="utf-8")
    return json.loads(text)


def _extract_servers_dict(data: dict, root_key: str, client: str) -> dict:
    """Extract servers as a dict from config data."""
    if root_key in data:
        section = data[root_key]
        if isinstance(section, dict):
            return dict(section)

    # VS Code nested format
    if client == "VS Code Copilot":
        mcp = data.get("mcp", {})
        if isinstance(mcp, dict):
            svrs = mcp.get("servers", {})
            if isinstance(svrs, dict):
                return dict(svrs)

    return {}


def _file_mtime(path: Path) -> float:
    """Get file modification time (for conflict resolution)."""
    try:
        return path.stat().st_mtime
    except Exception:
        return 0.0


def build_sync_plan(configs: List[DetectedConfig]) -> SyncPlan:
    """
    Build a sync plan by merging all detected configs.

    Merge strategy:
      - Union of all server names
      - If same server appears in multiple configs with different settings:
        newest file wins (by mtime)
      - Track all conflicts for reporting
    """
    server_sources: Dict[str, List[Tuple[DetectedConfig, dict]]] = {}

    for dc in configs:
        if dc.error:
            continue
        try:
            data = _read_config_data(dc)
            servers = _extract_servers_dict(data, dc.root_key, dc.client)

            for name, config in servers.items():
                if name not in server_sources:
                    server_sources[name] = []
                server_sources[name].append((dc, copy.deepcopy(config)))
        except Exception:
            continue

    merged: Dict[str, dict] = {}
    conflicts: List[dict] = []

    for name, sources in server_sources.items():
        if len(sources) == 1:
            merged[name] = sources[0][1]
        else:
            configs_json = [json.dumps(s[1], sort_keys=True) for s in sources]
            if len(set(configs_json)) == 1:
                merged[name] = sources[0][1]
            else:
                newest = max(sources, key=lambda s: _file_mtime(s[0].path))
                merged[name] = newest[1]
                conflicts.append({
                    "server": name,
                    "winner": str(newest[0].path),
                    "winner_client": newest[0].client,
                    "sources": [
                        {"client": s[0].client, "path": str(s[0].path)}
                        for s in sources
                    ],
                    "reason": "newest file wins",
                })

    changes_per_client: Dict[str, List[str]] = {}

    for dc in configs:
        if dc.error:
            continue

        try:
            data = _read_config_data(dc)
            current_servers = _extract_servers_dict(data, dc.root_key, dc.client)
        except Exception:
            continue

        changes = []
        path_key = str(dc.path)

        for name in merged:
            if name not in current_servers:
                changes.append(f"+ add server '{name}'")

        for name in merged:
            if name in current_servers:
                if json.dumps(merged[name], sort_keys=True) != json.dumps(current_servers[name], sort_keys=True):
                    changes.append(f"~ update server '{name}'")

        if changes:
            changes_per_client[path_key] = changes

    return SyncPlan(
        source_configs=configs,
        merged_servers=merged,
        changes_per_client=changes_per_client,
        conflicts=conflicts,
    )


# ============================================================================
# APPLY SYNC
# ============================================================================

def _inject_servers(data: dict, root_key: str, client: str,
                    merged: Dict[str, dict]) -> dict:
    """Inject merged servers into client config data, preserving other keys."""
    result = copy.deepcopy(data)

    if client == "VS Code Copilot" and root_key == "servers":
        if "mcp" in result:
            result["mcp"]["servers"] = copy.deepcopy(merged)
        else:
            result[root_key] = copy.deepcopy(merged)
    else:
        result[root_key] = copy.deepcopy(merged)

    return result


def apply_sync(plan: SyncPlan, dry_run: bool = True) -> str:
    """
    Apply the sync plan to all detected configs.

    Args:
        plan: The sync plan from build_sync_plan()
        dry_run: If True, only report changes without writing files.

    Returns:
        Human-readable report of changes made (or would be made).
    """
    lines = []

    if dry_run:
        lines.append("=== DRY RUN — No changes will be written ===\n")
    else:
        lines.append("=== APPLYING SYNC ===\n")

    lines.append(f"Merged servers: {len(plan.merged_servers)}")
    lines.append(f"Configs to update: {len(plan.changes_per_client)}")
    lines.append(f"Conflicts resolved: {len(plan.conflicts)}")
    lines.append("")

    if plan.conflicts:
        lines.append("Conflicts (resolved by newest-file-wins):")
        for c in plan.conflicts:
            lines.append(f"  Server '{c['server']}': {len(c['sources'])} versions")
            lines.append(f"    Winner: {c['winner_client']} ({c['winner']})  ")
            for s in c["sources"]:
                marker = " <-- winner" if s["path"] == c["winner"] else ""
                lines.append(f"    - {s['client']}: {s['path']}{marker}")
        lines.append("")

    for dc in plan.source_configs:
        if dc.error:
            continue

        path_key = str(dc.path)
        changes = plan.changes_per_client.get(path_key, [])

        if not changes:
            lines.append(f"[OK] {dc.client} ({dc.scope}): {dc.path}")
            lines.append("     Already in sync.")
            continue

        lines.append(f"[{'WOULD UPDATE' if dry_run else 'UPDATING'}] {dc.client} ({dc.scope}): {dc.path}")
        for change in changes:
            lines.append(f"     {change}")

        if not dry_run:
            try:
                data = _read_config_data(dc)
                updated = _inject_servers(data, dc.root_key, dc.client, plan.merged_servers)
                dc.path.write_text(
                    json.dumps(updated, indent=2, ensure_ascii=False) + "\n",
                    encoding="utf-8",
                )
                lines.append("     Written successfully.")
            except Exception as e:
                lines.append(f"     ERROR: {e}")

        lines.append("")

    if dry_run:
        lines.append(f"--- Dry run complete. {plan.total_changes} change(s) would be applied.")
        lines.append("    Run with --apply to write changes.")
    else:
        lines.append(f"--- Sync complete. {plan.total_changes} change(s) applied.")

    return "\n".join(lines)
