"""ReflACT Aggregate stage — hierarchical patch merging.

The Aggregate stage takes independently-generated patches from the Reflect
stage and merges them into a single coherent patch via hierarchical LLM calls.
Failure-driven patches take priority over success-driven ones.
"""
from __future__ import annotations

import json
from concurrent.futures import ThreadPoolExecutor, as_completed

from skillopt.model import chat_teacher
from skillopt.optimizer.meta_skill import format_meta_skill_context
from skillopt.optimizer.update_modes import (
    get_payload_items,
    is_full_rewrite_minibatch_mode,
    is_rewrite_mode,
    normalize_update_mode,
    payload_key,
    payload_label,
)
from skillopt.prompts import load_prompt
from skillopt.utils import extract_json


# ── Internal helpers ──────────────────────────────────────────────────────────

def _merge_batch(
    skill_content: str,
    patches: list[dict],
    system_prompt: str,
    update_mode: str,
    meta_skill_context: str = "",
    level: int = 1,
) -> dict:
    """Call teacher LLM to merge a batch of patches into one."""
    patches_text = json.dumps(patches, ensure_ascii=False, indent=2)
    user = (
        f"## Current Skill\n{skill_content}\n\n"
        f"## Patches to merge ({len(patches)} total, merge level {level})\n{patches_text}"
    )
    teacher_ctx = format_meta_skill_context(meta_skill_context)
    if teacher_ctx:
        user = f"{teacher_ctx}\n\n{user}"
    try:
        response, _ = chat_teacher(
            system=system_prompt,
            user=user,
            max_completion_tokens=64000 if is_full_rewrite_minibatch_mode(update_mode) else 4096,
            retries=3,
            stage="merge",
        )
        merged = extract_json(response)
        key = payload_key(update_mode)
        if merged and key in merged:
            for e in merged.get(key, []):
                e["merge_level"] = level
            return merged
    except Exception:  # noqa: BLE001
        pass
    # Fallback: concatenate all edits
    all_edits = []
    for p in patches:
        for e in get_payload_items(p, update_mode):
            e.setdefault("merge_level", level)
            all_edits.append(e)
    return {"reasoning": "fallback concatenation", payload_key(update_mode): all_edits}


def _hierarchical_merge(
    skill_content: str,
    patches: list[dict],
    system_prompt: str,
    update_mode: str,
    batch_size: int,
    verbose: bool,
    label: str = "",
    workers: int = 16,
    meta_skill_context: str = "",
) -> dict:
    """Hierarchically merge N patches using the given system prompt.

    Same-level batches are executed in PARALLEL via ThreadPoolExecutor.
    """
    if not patches:
        return {"reasoning": "no patches", payload_key(update_mode): []}
    if len(patches) == 1:
        return patches[0]

    current = list(patches)
    level = 0
    while len(current) > 1:
        level += 1
        batches: list[tuple[int, list[dict]]] = []
        for i in range(0, len(current), batch_size):
            batch = current[i : i + batch_size]
            batches.append((i, batch))

        if verbose:
            print(
                f"    [aggregate {label}] level={level}  "
                f"{len(current)} patches → {len(batches)} batches "
                f"(parallel, batch_size={batch_size})"
            )

        next_level: list[dict | None] = [None] * len(batches)

        to_merge: list[tuple[int, list[dict]]] = []
        for idx, (i, batch) in enumerate(batches):
            if len(batch) == 1:
                next_level[idx] = batch[0]
            else:
                to_merge.append((idx, batch))

        if to_merge:
            with ThreadPoolExecutor(max_workers=workers) as ex:
                futs = {
                    ex.submit(
                        _merge_batch, skill_content, batch, system_prompt, update_mode,
                        meta_skill_context, level,
                    ): idx
                    for idx, batch in to_merge
                }
                for fut in as_completed(futs):
                    idx = futs[fut]
                    next_level[idx] = fut.result()
                    if verbose:
                        batch_i, batch_data = batches[idx]
                        n_edits = len(get_payload_items(next_level[idx], update_mode))
                        print(
                            f"      [aggregate {label}] level={level} "
                            f"batch [{batch_i}:{batch_i+len(batch_data)}] "
                            f"→ 1 patch ({n_edits} {payload_label(update_mode)})"
                        )

        current = [x for x in next_level if x is not None]

    return current[0]


# ── Public API ────────────────────────────────────────────────────────────────

def merge_patches(
    skill_content: str,
    failure_patches: list[dict],
    success_patches: list[dict],
    batch_size: int = 8,
    verbose: bool = True,
    workers: int = 16,
    update_mode: str = "patch",
    meta_skill_context: str = "",
) -> dict:
    """Failure-first hierarchical merge with support count tracking.

    1. Merge failure patches independently (parallel)
    2. Merge success patches independently (parallel)
    3. Final merge: combine both groups with failure priority

    Returns a merged :class:`~skillopt.types.Patch` dict (``edits`` + ``reasoning``).
    """
    if verbose:
        print(
            f"    [3/6 AGGREGATE] "
            f"failure={len(failure_patches)} success={len(success_patches)} "
            f"(parallel, workers={workers})"
        )

    update_mode = normalize_update_mode(update_mode)
    if is_full_rewrite_minibatch_mode(update_mode):
        merge_failure_prompt = load_prompt("merge_failure_full_rewrite")
        merge_success_prompt = load_prompt("merge_success_full_rewrite")
        merge_final_prompt = load_prompt("merge_final_full_rewrite")
    elif is_rewrite_mode(update_mode):
        merge_failure_prompt = load_prompt("merge_failure_rewrite")
        merge_success_prompt = load_prompt("merge_success_rewrite")
        merge_final_prompt = load_prompt("merge_final_rewrite")
    else:
        merge_failure_prompt = load_prompt("merge_failure")
        merge_success_prompt = load_prompt("merge_success")
        merge_final_prompt = load_prompt("merge_final")

    failure_merged = _hierarchical_merge(
        skill_content, failure_patches, merge_failure_prompt, update_mode,
        batch_size, verbose, label="failure", workers=workers,
        meta_skill_context=meta_skill_context,
    )

    success_merged = _hierarchical_merge(
        skill_content, success_patches, merge_success_prompt, update_mode,
        batch_size, verbose, label="success", workers=workers,
        meta_skill_context=meta_skill_context,
    )

    f_edits = get_payload_items(failure_merged, update_mode)
    s_edits = get_payload_items(success_merged, update_mode)

    if not f_edits and not s_edits:
        return {"reasoning": "no updates from either group", payload_key(update_mode): []}
    if not s_edits:
        return failure_merged
    if not f_edits:
        return success_merged

    combined_patches = [failure_merged, success_merged]
    combined_text = json.dumps(combined_patches, ensure_ascii=False, indent=2)
    if is_full_rewrite_minibatch_mode(update_mode):
        item_label = payload_label(update_mode)
        user = (
            f"## Current Skill\n{skill_content}\n\n"
            f"## Two pre-merged candidate groups to combine\n"
            f"Group 1 (from failed trajectories): "
            f"{len(f_edits)} {item_label}\n"
            f"Group 2 (from successful trajectories): "
            f"{len(s_edits)} {item_label}\n\n"
            f"{combined_text}"
        )
    else:
        user = (
            f"## Current Skill\n{skill_content}\n\n"
            f"## Two pre-merged patch groups to combine\n"
            f"Group 1 (failure-driven, HIGH priority): "
            f"{len(f_edits)} edits\n"
            f"Group 2 (success-driven, lower priority): "
            f"{len(s_edits)} edits\n\n"
            f"{combined_text}"
        )
    teacher_ctx = format_meta_skill_context(meta_skill_context)
    if teacher_ctx:
        user = f"{teacher_ctx}\n\n{user}"
    try:
        response, _ = chat_teacher(
            system=merge_final_prompt,
            user=user,
            max_completion_tokens=64000 if is_full_rewrite_minibatch_mode(update_mode) else 4096,
            retries=3,
            stage="merge",
        )
        final = extract_json(response)
        key = payload_key(update_mode)
        if final and key in final:
            if verbose:
                print(
                    f"    [aggregate final] "
                    f"{len(f_edits)}+{len(s_edits)} → {len(final[key])} {payload_label(update_mode)}"
                )
            return final
    except Exception:  # noqa: BLE001
        pass

    return {
        "reasoning": "fallback: failure first, then success",
        payload_key(update_mode): f_edits + s_edits,
    }
