from __future__ import annotations

import hashlib
import json
from collections import defaultdict
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Literal

from agentic_pd_hybrid.trace import TraceRequest, load_trace


SampleProfile = Literal["default", "small-append"]


@dataclass(frozen=True)
class SessionSampleConfig:
    trace_path: Path
    output_path: Path
    target_duration_s: float = 600.0
    start_time_s: float = 0.0
    session_sample_rate: float = 1.0
    min_turns: int = 1
    max_requests: int | None = None
    profile: SampleProfile = "default"
    min_initial_input_tokens: int | None = None
    max_initial_input_tokens: int | None = None
    max_append_input_tokens: int | None = None
    max_output_tokens: int | None = None
    min_overlap_ratio: float | None = None


@dataclass(frozen=True)
class SessionSampleSummary:
    input_trace_path: str
    output_trace_path: str
    request_count: int
    session_count: int
    multi_turn_session_count: int
    start_time_s: float
    end_time_s: float
    sampled_duration_s: float
    session_sample_rate: float
    min_turns: int
    profile: str
    min_initial_input_tokens: int | None
    max_initial_input_tokens: int | None
    max_append_input_tokens: int | None
    max_output_tokens: int | None
    min_overlap_ratio: float | None
    mean_append_input_tokens: float | None
    mean_turn_overlap_ratio: float | None


def sample_trace_sessions(config: SessionSampleConfig) -> SessionSampleSummary:
    requests = load_trace(config.trace_path)
    sessions: dict[str, list[TraceRequest]] = defaultdict(list)
    for request in requests:
        sessions[request.session_id].append(request)

    filters = _resolve_filters(config)
    eligible_sessions = {
        session_id: session_requests
        for session_id, session_requests in sessions.items()
        if len(session_requests) >= filters.min_turns
        and _session_matches_filters(session_requests, filters)
        and _keep_session(session_id, config.session_sample_rate)
    }
    ordered_sessions = sorted(
        eligible_sessions.values(),
        key=lambda session_requests: session_requests[0].timestamp_s,
    )

    selected_requests: list[TraceRequest] = []
    sampled_start: float | None = None
    sampled_end: float | None = None
    for session_requests in ordered_sessions:
        session_first = session_requests[0].timestamp_s
        if session_first < config.start_time_s:
            continue

        if sampled_start is None:
            sampled_start = session_first

        selected_requests.extend(session_requests)
        sampled_end = max(request.timestamp_s for request in session_requests)

        if config.max_requests is not None and len(selected_requests) >= config.max_requests:
            break
        if sampled_end - sampled_start >= config.target_duration_s:
            break

    selected_requests.sort(key=lambda request: request.timestamp_s)
    if config.max_requests is not None:
        selected_requests = selected_requests[: config.max_requests]

    if not selected_requests:
        raise ValueError("Sampling produced no requests; adjust the sampling arguments")

    config.output_path.parent.mkdir(parents=True, exist_ok=True)
    with config.output_path.open("w", encoding="utf-8") as handle:
        for request in selected_requests:
            payload = {
                "request_id": request.request_id,
                "session_id": request.session_id,
                "chat_id": request.chat_id,
                "parent_chat_id": request.parent_chat_id,
                "timestamp": request.timestamp_s,
                "input_length": request.input_length,
                "output_length": request.output_length,
                "type": request.request_type,
                "turn": request.turn_id,
                "hash_ids": list(request.hash_ids),
            }
            handle.write(json.dumps(payload, sort_keys=True) + "\n")

    selected_session_ids = {request.session_id for request in selected_requests}
    selected_session_requests = [
        eligible_sessions[session_id] for session_id in selected_session_ids
    ]
    append_lengths = [
        length
        for session_requests in selected_session_requests
        for length in _turn_append_lengths(session_requests)
    ]
    overlap_ratios = [
        ratio
        for session_requests in selected_session_requests
        for ratio in _turn_overlap_ratios(session_requests)
    ]
    summary = SessionSampleSummary(
        input_trace_path=str(config.trace_path),
        output_trace_path=str(config.output_path),
        request_count=len(selected_requests),
        session_count=len(selected_session_ids),
        multi_turn_session_count=sum(
            1
            for session_id in selected_session_ids
            if len(eligible_sessions[session_id]) > 1
        ),
        start_time_s=selected_requests[0].timestamp_s,
        end_time_s=selected_requests[-1].timestamp_s,
        sampled_duration_s=selected_requests[-1].timestamp_s
        - selected_requests[0].timestamp_s,
        session_sample_rate=config.session_sample_rate,
        min_turns=filters.min_turns,
        profile=config.profile,
        min_initial_input_tokens=filters.min_initial_input_tokens,
        max_initial_input_tokens=filters.max_initial_input_tokens,
        max_append_input_tokens=filters.max_append_input_tokens,
        max_output_tokens=filters.max_output_tokens,
        min_overlap_ratio=filters.min_overlap_ratio,
        mean_append_input_tokens=_mean(append_lengths),
        mean_turn_overlap_ratio=_mean(overlap_ratios),
    )
    summary_path = config.output_path.with_suffix(config.output_path.suffix + ".summary.json")
    with summary_path.open("w", encoding="utf-8") as handle:
        json.dump(asdict(summary), handle, indent=2, sort_keys=True)
    return summary


@dataclass(frozen=True)
class _ResolvedFilters:
    min_turns: int
    min_initial_input_tokens: int | None
    max_initial_input_tokens: int | None
    max_append_input_tokens: int | None
    max_output_tokens: int | None
    min_overlap_ratio: float | None


def _resolve_filters(config: SessionSampleConfig) -> _ResolvedFilters:
    if config.profile == "default":
        return _ResolvedFilters(
            min_turns=config.min_turns,
            min_initial_input_tokens=config.min_initial_input_tokens,
            max_initial_input_tokens=config.max_initial_input_tokens,
            max_append_input_tokens=config.max_append_input_tokens,
            max_output_tokens=config.max_output_tokens,
            min_overlap_ratio=config.min_overlap_ratio,
        )

    if config.profile != "small-append":
        raise ValueError(f"Unsupported sample profile: {config.profile}")

    return _ResolvedFilters(
        min_turns=max(config.min_turns, 2),
        min_initial_input_tokens=(
            2048
            if config.min_initial_input_tokens is None
            else config.min_initial_input_tokens
        ),
        max_initial_input_tokens=(
            16000
            if config.max_initial_input_tokens is None
            else config.max_initial_input_tokens
        ),
        max_append_input_tokens=(
            2048
            if config.max_append_input_tokens is None
            else config.max_append_input_tokens
        ),
        max_output_tokens=(
            2048 if config.max_output_tokens is None else config.max_output_tokens
        ),
        min_overlap_ratio=(
            0.75 if config.min_overlap_ratio is None else config.min_overlap_ratio
        ),
    )


def _session_matches_filters(
    session_requests: list[TraceRequest],
    filters: _ResolvedFilters,
) -> bool:
    ordered = sorted(
        session_requests,
        key=lambda request: (request.timestamp_s, request.turn_id, request.chat_id),
    )
    if not ordered:
        return False

    initial = ordered[0]
    if (
        filters.min_initial_input_tokens is not None
        and initial.input_length < filters.min_initial_input_tokens
    ):
        return False
    if (
        filters.max_initial_input_tokens is not None
        and initial.input_length > filters.max_initial_input_tokens
    ):
        return False
    if filters.max_output_tokens is not None and any(
        request.output_length > filters.max_output_tokens for request in ordered
    ):
        return False

    append_lengths = _turn_append_lengths(ordered)
    if filters.max_append_input_tokens is not None and any(
        append_length <= 0 or append_length > filters.max_append_input_tokens
        for append_length in append_lengths
    ):
        return False

    overlap_ratios = _turn_overlap_ratios(ordered)
    if filters.min_overlap_ratio is not None and any(
        overlap_ratio < filters.min_overlap_ratio for overlap_ratio in overlap_ratios
    ):
        return False

    return True


def _turn_append_lengths(session_requests: list[TraceRequest]) -> list[int]:
    ordered = sorted(
        session_requests,
        key=lambda request: (request.timestamp_s, request.turn_id, request.chat_id),
    )
    return [
        current.input_length - (previous.input_length + previous.output_length)
        for previous, current in zip(ordered, ordered[1:], strict=False)
    ]


def _turn_overlap_ratios(session_requests: list[TraceRequest]) -> list[float]:
    ordered = sorted(
        session_requests,
        key=lambda request: (request.timestamp_s, request.turn_id, request.chat_id),
    )
    ratios: list[float] = []
    for previous, current in zip(ordered, ordered[1:], strict=False):
        if not current.hash_ids:
            ratios.append(0.0)
            continue
        previous_blocks = set(previous.hash_ids)
        overlap = sum(1 for block in current.hash_ids if block in previous_blocks)
        ratios.append(overlap / len(current.hash_ids))
    return ratios


def _mean(values: list[int] | list[float]) -> float | None:
    if not values:
        return None
    return sum(values) / len(values)


def _keep_session(session_id: str, sample_rate: float) -> bool:
    if sample_rate >= 1.0:
        return True
    if sample_rate <= 0.0:
        return False
    digest = hashlib.blake2b(session_id.encode("utf-8"), digest_size=8).digest()
    bucket = int.from_bytes(digest, byteorder="big", signed=False) / 2**64
    return bucket < sample_rate
