"""
cortex/hippo/compaction/condensed.py — Condensed pass functions (merge same-depth summaries).
"""

from __future__ import annotations

import json
import logging
from typing import Optional

from cortex.hippo.config import HippoConfig
from cortex.hippo.models import ContextItemRecord
from cortex.hippo.store.summary_store import SummaryStore
from cortex.hippo.summarize import SummarizeOptions

from .helpers import estimate_tokens, format_timestamp
from .leaf import _resolve_fresh_tail_ordinal
from .types import (
    CondensedChunkSelection,
    CondensedPhaseCandidate,
    PassResult,
    SummarizeFn,
)
from .utils import _get_summary_by_context_item, _summarize_with_escalation

logger = logging.getLogger(__name__)


async def _select_oldest_chunk_at_depth(
    conversation_id: int,
    target_depth: int,
    summary_store: SummaryStore,
    config: HippoConfig,
    fresh_tail_ordinal: Optional[float] = None,
) -> CondensedChunkSelection:
    """Select oldest contiguous summary chunk at a specific depth."""
    context_items = await summary_store.get_context_items(conversation_id)
    if fresh_tail_ordinal is None:
        fresh_tail_ordinal = _resolve_fresh_tail_ordinal(context_items, config)

    chunk: list[ContextItemRecord] = []
    chunk_tokens = 0
    owner_id = None  # Will be determined from first summary

    for item in context_items:
        if item.ordinal >= fresh_tail_ordinal:
            break
        if item.item_type != "summary" or not item.summary_id:
            if chunk:
                break  # Non-summary interrupts contiguous run
            continue

        # Look up summary to check depth
        # We need owner_id — infer from existing summaries
        if owner_id is None:
            # Fetch the summary to get owner_id
            summary = await _get_summary_by_context_item(item, summary_store)
            if not summary:
                continue
            owner_id = summary.owner_id
            if summary.depth != target_depth:
                continue
            chunk.append(item)
            chunk_tokens += summary.token_count
        else:
            summary = await _get_summary_by_context_item(item, summary_store)
            if not summary or summary.depth != target_depth:
                if chunk:
                    break
                continue
            chunk.append(item)
            chunk_tokens += summary.token_count

    return CondensedChunkSelection(items=chunk, summary_tokens=chunk_tokens)


async def _select_shallowest_condensation_candidate(
    conversation_id: int,
    hard_trigger: bool,
    summary_store: SummaryStore,
    config: HippoConfig,
) -> Optional[CondensedPhaseCandidate]:
    """Find shallowest depth with an eligible same-depth summary chunk."""
    context_items = await summary_store.get_context_items(conversation_id)
    fresh_tail_ordinal = _resolve_fresh_tail_ordinal(context_items, config)

    depths = await summary_store.get_distinct_depths_in_context(
        conversation_id, fresh_tail_ordinal
    )

    for depth in depths:
        fanout = (
            config.leaf_min_fanout
            if depth == 0
            else (
                config.condensed_min_fanout_hard
                if hard_trigger
                else config.condensed_min_fanout
            )
        )

        chunk = await _select_oldest_chunk_at_depth(
            conversation_id, depth, summary_store, config, fresh_tail_ordinal
        )

        if (
            len(chunk.items) >= fanout
            and chunk.summary_tokens >= config.condensed_min_chunk_tokens
        ):
            return CondensedPhaseCandidate(
                target_depth=depth, chunk=chunk
            )

    return None


async def _condensed_pass(
    conversation_id: int,
    summary_items: list[ContextItemRecord],
    target_depth: int,
    summarize: SummarizeFn,
    summary_store: SummaryStore,
) -> PassResult:
    """Merge same-depth summaries into depth+1 condensed summary."""
    texts = []
    parent_ids = []
    total_tokens = 0
    total_descendant_count = 0
    total_descendant_tokens = 0
    total_source_msg_tokens = 0
    all_file_ids: list[str] = []
    earliest_at = None
    latest_at = None
    owner_id = "default"

    for item in summary_items:
        if not item.summary_id:
            continue
        # Need to look up summary — get owner_id from context
        summary = await _get_summary_by_context_item(item, summary_store)
        if not summary:
            continue

        owner_id = summary.owner_id
        parent_ids.append(summary.summary_id)
        total_tokens += summary.token_count
        total_descendant_count += summary.descendant_count + 1
        total_descendant_tokens += summary.descendant_token_count + summary.token_count
        total_source_msg_tokens += summary.source_message_token_count

        # Parse file_ids
        try:
            fids = json.loads(summary.file_ids) if summary.file_ids else []
            all_file_ids.extend(fids)
        except (json.JSONDecodeError, TypeError):
            pass

        ts_earliest = format_timestamp(summary.earliest_at or summary.created_at)
        ts_latest = format_timestamp(summary.latest_at or summary.created_at)
        texts.append(f"[{ts_earliest} - {ts_latest}]\n{summary.content}")

        if earliest_at is None or (summary.earliest_at and summary.earliest_at < earliest_at):
            earliest_at = summary.earliest_at or summary.created_at
        if latest_at is None or (summary.latest_at and summary.latest_at > latest_at):
            latest_at = summary.latest_at or summary.created_at

    concatenated = "\n\n".join(texts)

    new_depth = target_depth + 1

    # Fetch the most recent condensed summary at new_depth for continuity context
    # (equivalent to lossless-claw's previousSummary for depth-0 condensation)
    previous_summary_content: Optional[str] = None
    try:
        all_context_items = await summary_store.get_context_items(conversation_id)
        for item in reversed(all_context_items):
            # Skip items that are part of the current chunk being condensed
            if item in summary_items:
                continue
            if item.item_type == "summary" and item.summary_id:
                candidate = await _get_summary_by_context_item(item, summary_store)
                if candidate and candidate.depth == new_depth:
                    previous_summary_content = candidate.content
                    break
    except Exception:
        pass  # Continuity context is best-effort; compaction proceeds without it

    summary_result = await _summarize_with_escalation(
        concatenated, summarize,
        SummarizeOptions(
            is_condensed=True,
            depth=new_depth,
            previous_summary=previous_summary_content,
        ),
    )

    summary_content = summary_result.content
    level = summary_result.mode_used
    quality_score = summary_result.quality_score

    token_count = estimate_tokens(summary_content)

    record = await summary_store.create_summary(
        conversation_id=conversation_id,
        owner_id=owner_id,
        kind="condensed",
        depth=new_depth,
        content=summary_content,
        token_count=token_count,
        quality_score=quality_score,
        earliest_at=earliest_at,
        latest_at=latest_at,
        descendant_count=total_descendant_count,
        descendant_token_count=total_descendant_tokens,
        source_message_token_count=total_source_msg_tokens,
    )

    actual_summary_id = record.summary_id

    # Link to parent summaries
    await summary_store.add_summary_parents(actual_summary_id, parent_ids)

    # Replace context range
    start_ordinal = min(item.ordinal for item in summary_items)
    end_ordinal = max(item.ordinal for item in summary_items)
    await summary_store.replace_context_range_with_summary(
        conversation_id, start_ordinal, end_ordinal, actual_summary_id
    )

    return PassResult(
        summary_id=actual_summary_id,
        level=level,
        content=summary_content,
        tokens_before=total_tokens,
        tokens_after=token_count,
        quality_score=quality_score,
    )
