from __future__ import annotations

import json
import re
from dataclasses import dataclass

from .agent_prompting import SYSTEM_PROMPT_DYNAMIC_BOUNDARY
from .agent_session import AgentMessage, AgentSessionState
from .tokenizer_runtime import describe_token_counter, count_tokens

_PATH_HEADER_RE = re.compile(r'^## ((?:/|[A-Za-z]:[\\/]).+)$', re.MULTILINE)


@dataclass(frozen=True)
class UsageEntry:
    name: str
    tokens: int


@dataclass(frozen=True)
class ToolUsageEntry:
    name: str
    call_tokens: int
    result_tokens: int


@dataclass(frozen=True)
class MessageBreakdown:
    user_message_tokens: int
    assistant_message_tokens: int
    tool_call_tokens: int
    tool_result_tokens: int
    user_context_tokens: int
    tool_calls_by_type: tuple[ToolUsageEntry, ...]


@dataclass(frozen=True)
class ContextUsageReport:
    model: str
    total_tokens: int
    raw_max_tokens: int
    percentage: float
    strategy: str
    message_count: int
    categories: tuple[UsageEntry, ...]
    system_prompt_sections: tuple[UsageEntry, ...]
    user_context_entries: tuple[UsageEntry, ...]
    system_context_entries: tuple[UsageEntry, ...]
    memory_files: tuple[UsageEntry, ...]
    message_breakdown: MessageBreakdown
    token_counter_backend: str
    token_counter_source: str
    token_counter_accurate: bool


def estimate_tokens(text: str, model: str | None = None) -> int:
    return count_tokens(text, model)


def infer_context_window(model: str) -> int:
    lowered = model.lower()
    if 'qwen3-coder' in lowered:
        return 256_000
    if 'devstral' in lowered:
        return 256_000
    if 'qwen' in lowered:
        return 131_072
    if 'claude' in lowered:
        return 200_000
    if 'gpt-4.1' in lowered or 'gpt-4o' in lowered:
        return 128_000
    return 128_000


def collect_context_usage(
    *,
    session: AgentSessionState,
    model: str,
    strategy: str,
) -> ContextUsageReport:
    raw_max_tokens = infer_context_window(model)
    token_counter = describe_token_counter(model)
    count = lambda text: estimate_tokens(text, model)  # noqa: E731
    system_prompt_sections = tuple(
        UsageEntry(name=_section_name(part, idx), tokens=count(part))
        for idx, part in enumerate(session.system_prompt_parts, start=1)
    )
    system_context_entries = tuple(
        UsageEntry(name=key, tokens=count(f'{key}: {value}'))
        for key, value in session.system_context.items()
        if value
    )
    user_context_entries = tuple(
        UsageEntry(name=key, tokens=count(_render_user_context_chunk(key, value)))
        for key, value in session.user_context.items()
        if value
    )
    memory_files = tuple(_parse_memory_usage(session.user_context.get('claudeMd'), model=model))

    user_context_tokens = sum(entry.tokens for entry in user_context_entries)
    system_prompt_tokens = (
        sum(entry.tokens for entry in system_prompt_sections)
        + sum(entry.tokens for entry in system_context_entries)
    )

    conversation_user_tokens = 0
    assistant_tokens = 0
    tool_call_tokens = 0
    tool_result_tokens = 0
    tool_usage: dict[str, list[int]] = {}

    for index, message in enumerate(session.messages):
        if index == 0 and message.role == 'system':
            continue
        if _is_user_context_message(session, index, message):
            continue
        if message.role == 'user':
            conversation_user_tokens += count(message.content)
            continue
        if message.role == 'assistant':
            assistant_tokens += count(message.content)
            for tool_call in message.tool_calls:
                serialized = json.dumps(tool_call, ensure_ascii=True)
                tokens = count(serialized)
                tool_call_tokens += tokens
                tool_name = _extract_tool_call_name(tool_call)
                call_totals = tool_usage.setdefault(tool_name, [0, 0])
                call_totals[0] += tokens
            continue
        if message.role == 'tool':
            tokens = count(message.content)
            tool_result_tokens += tokens
            result_totals = tool_usage.setdefault(message.name or 'tool', [0, 0])
            result_totals[1] += tokens

    categories = [
        UsageEntry('System prompt', system_prompt_tokens),
        UsageEntry('User context', user_context_tokens),
        UsageEntry('User messages', conversation_user_tokens),
        UsageEntry('Assistant messages', assistant_tokens),
        UsageEntry('Tool calls', tool_call_tokens),
        UsageEntry('Tool results', tool_result_tokens),
    ]
    total_tokens = sum(entry.tokens for entry in categories)
    free_space = max(raw_max_tokens - total_tokens, 0)
    categories.append(UsageEntry('Free space', free_space))

    tool_calls_by_type = tuple(
        ToolUsageEntry(
            name=name,
            call_tokens=values[0],
            result_tokens=values[1],
        )
        for name, values in sorted(
            tool_usage.items(),
            key=lambda item: (item[1][0] + item[1][1], item[0]),
            reverse=True,
        )
        if values[0] or values[1]
    )
    percentage = (total_tokens / raw_max_tokens * 100) if raw_max_tokens else 0.0
    return ContextUsageReport(
        model=model,
        total_tokens=total_tokens,
        raw_max_tokens=raw_max_tokens,
        percentage=percentage,
        strategy=strategy,
        message_count=len(session.messages),
        categories=tuple(categories),
        system_prompt_sections=system_prompt_sections,
        user_context_entries=user_context_entries,
        system_context_entries=system_context_entries,
        memory_files=memory_files,
        message_breakdown=MessageBreakdown(
            user_message_tokens=conversation_user_tokens,
            assistant_message_tokens=assistant_tokens,
            tool_call_tokens=tool_call_tokens,
            tool_result_tokens=tool_result_tokens,
            user_context_tokens=user_context_tokens,
            tool_calls_by_type=tool_calls_by_type,
        ),
        token_counter_backend=token_counter.backend,
        token_counter_source=token_counter.source,
        token_counter_accurate=token_counter.accurate,
    )


def format_context_usage(report: ContextUsageReport) -> str:
    lines = [
        '## Context Usage',
        '',
        f'**Model:** {report.model}  ',
        f'**Estimated tokens:** {_format_tokens(report.total_tokens)} / {_format_tokens(report.raw_max_tokens)} ({report.percentage:.1f}%)  ',
        f'**Token counter:** {report.token_counter_backend} ({report.token_counter_source}){" [accurate]" if report.token_counter_accurate else " [fallback]"}  ',
        f'**Context strategy:** {report.strategy}  ',
        f'**Messages in session:** {report.message_count}',
        '',
    ]

    visible_categories = [entry for entry in report.categories if entry.tokens > 0]
    if visible_categories:
        lines.extend(
            [
                '### Estimated usage by category',
                '',
                '| Category | Tokens | Percentage |',
                '|----------|--------|------------|',
            ]
        )
        for entry in visible_categories:
            percent = (entry.tokens / report.raw_max_tokens * 100) if report.raw_max_tokens else 0.0
            lines.append(f'| {entry.name} | {_format_tokens(entry.tokens)} | {percent:.1f}% |')
        lines.append('')

    if report.system_prompt_sections:
        lines.extend(
            [
                '### System Prompt Sections',
                '',
                '| Section | Tokens |',
                '|---------|--------|',
            ]
        )
        for entry in report.system_prompt_sections:
            lines.append(f'| {entry.name} | {_format_tokens(entry.tokens)} |')
        lines.append('')

    if report.user_context_entries:
        lines.extend(
            [
                '### User Context',
                '',
                '| Entry | Tokens |',
                '|-------|--------|',
            ]
        )
        for entry in report.user_context_entries:
            lines.append(f'| {entry.name} | {_format_tokens(entry.tokens)} |')
        lines.append('')

    if report.system_context_entries:
        lines.extend(
            [
                '### System Context',
                '',
                '| Entry | Tokens |',
                '|-------|--------|',
            ]
        )
        for entry in report.system_context_entries:
            lines.append(f'| {entry.name} | {_format_tokens(entry.tokens)} |')
        lines.append('')

    if report.memory_files:
        lines.extend(
            [
                '### Memory Files',
                '',
                '| Path | Tokens |',
                '|------|--------|',
            ]
        )
        for entry in report.memory_files:
            lines.append(f'| {entry.name} | {_format_tokens(entry.tokens)} |')
        lines.append('')

    breakdown = report.message_breakdown
    lines.extend(
        [
            '### Message Breakdown',
            '',
            '| Category | Tokens |',
            '|----------|--------|',
            f'| User context reminder | {_format_tokens(breakdown.user_context_tokens)} |',
            f'| User messages | {_format_tokens(breakdown.user_message_tokens)} |',
            f'| Assistant messages | {_format_tokens(breakdown.assistant_message_tokens)} |',
            f'| Tool calls | {_format_tokens(breakdown.tool_call_tokens)} |',
            f'| Tool results | {_format_tokens(breakdown.tool_result_tokens)} |',
            '',
        ]
    )

    if breakdown.tool_calls_by_type:
        lines.extend(
            [
                '#### Top Tools',
                '',
                '| Tool | Call Tokens | Result Tokens |',
                '|------|-------------|---------------|',
            ]
        )
        for entry in breakdown.tool_calls_by_type:
            lines.append(
                f'| {entry.name} | {_format_tokens(entry.call_tokens)} | {_format_tokens(entry.result_tokens)} |'
            )
        lines.append('')

    while lines and lines[-1] == '':
        lines.pop()
    return '\n'.join(lines)


def _section_name(part: str, index: int) -> str:
    stripped = part.strip()
    if stripped == SYSTEM_PROMPT_DYNAMIC_BOUNDARY:
        return 'Dynamic boundary'
    first_line = stripped.splitlines()[0] if stripped else ''
    if first_line.startswith('#'):
        return first_line.lstrip('#').strip() or f'Section {index}'
    return f'Section {index}'


def _render_user_context_chunk(key: str, value: str) -> str:
    return f'# {key}\n{value}'


def _extract_tool_call_name(tool_call: dict[str, object]) -> str:
    function_block = tool_call.get('function')
    if isinstance(function_block, dict):
        name = function_block.get('name')
        if isinstance(name, str) and name:
            return name
    return 'unknown'


def _is_user_context_message(
    session: AgentSessionState,
    index: int,
    message: AgentMessage,
) -> bool:
    if not session.user_context:
        return False
    return (
        index == 1
        and message.role == 'user'
        and message.content.startswith('<system-reminder>')
    )


def _parse_memory_usage(claude_md: str | None, *, model: str | None = None) -> list[UsageEntry]:
    if not claude_md:
        return []
    matches = list(_PATH_HEADER_RE.finditer(claude_md))
    if not matches:
        return []
    entries: list[UsageEntry] = []
    for idx, match in enumerate(matches):
        start = match.end()
        end = matches[idx + 1].start() if idx + 1 < len(matches) else len(claude_md)
        content = claude_md[start:end].strip()
        entries.append(UsageEntry(name=match.group(1), tokens=estimate_tokens(content, model)))
    return entries


def _format_tokens(value: int) -> str:
    return f'{value:,}'
