"""Java line profiler optimizer module.

This module handles line profiler-guided optimization for Java code.
"""

from __future__ import annotations

import asyncio
import logging
import re
import uuid
from pathlib import Path
from typing import TYPE_CHECKING

import sentry_sdk
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam

from aiservice.analytics.posthog import ph
from aiservice.env_specific import debug_log_sensitive_data, debug_log_sensitive_data_from_callable
from aiservice.llm import llm_client
from aiservice.llm_models import OPTIMIZE_MODEL
from aiservice.validators.java_validator import validate_java_syntax
from core.languages.java.optimizer import is_multi_context_java
from core.shared.context_helpers import (
    extract_code_and_explanation,
    group_code,
    normalize_c_style_code,
    split_markdown_code,
)
from core.shared.optimizer_config import MAX_OPTIMIZER_LP_CALLS, get_model_distribution
from core.shared.optimizer_schemas import OptimizeResponseItemSchema

if TYPE_CHECKING:
    from openai.types.chat import ChatCompletionMessageParam

    from aiservice.llm_models import LLM


# Get the prompts directory
current_dir = Path(__file__).parent
JAVA_PROMPTS_DIR = current_dir / "prompts" / "optimizer"

# Load Java system prompt
JAVA_SYSTEM_PROMPT_TEXT = (JAVA_PROMPTS_DIR / "system_prompt.md").read_text()

# Pattern to extract code blocks from Java LLM response (handles optional :filename suffix)
JAVA_CODE_PATTERN = re.compile(r"```(?:java)(?::[^\n]*)?\s*\n(.*?)```", re.MULTILINE | re.DOTALL)

# Pattern to extract code blocks with file paths (multi-file context)
JAVA_CODE_WITH_PATH_PATTERN = re.compile(r"```(?:java):([^\n]+)\n(.*?)```", re.MULTILINE | re.DOTALL)

# Line profiler context prompt for Java
JAVA_LINE_PROF_CONTEXT = """
Here are the results of the line profiling of the Java code you will be optimizing.
The profiling data shows:
- Line numbers with execution counts (hits)
- Time spent on each line (in nanoseconds)
- Percentage of total time per line

Use this data to identify performance bottlenecks and focus your optimization on the hottest code paths.

{line_profiler_results}
"""


def extract_java_code_and_explanation(content: str, is_multi_file: bool = False) -> tuple[str | dict[str, str], str]:
    return extract_code_and_explanation(content, JAVA_CODE_PATTERN, JAVA_CODE_WITH_PATH_PATTERN, is_multi_file)


async def optimize_java_code_line_profiler_single(
    user_id: str,
    trace_id: str,
    source_code: str,
    line_profiler_results: str,
    dependency_code: str | None = None,
    optimize_model: LLM = OPTIMIZE_MODEL,
    language_version: str = "17",
    call_sequence: int | None = None,
) -> tuple[OptimizeResponseItemSchema | None, float | None, str]:
    """Optimize Java code using LLMs with line profiler guidance."""
    logging.info("/optimize-line-profiler: Optimizing Java code.")
    debug_log_sensitive_data(f"Optimizing Java code for user {user_id}:\n{source_code}")

    # Check if source code is multi-file format
    is_multi_file = is_multi_context_java(source_code)
    original_file_to_code: dict[str, str] = {}

    if is_multi_file:
        original_file_to_code = split_markdown_code(source_code, "java")
        logging.info(
            "Multi-file context detected with %d files: %s",
            len(original_file_to_code),
            list(original_file_to_code.keys()),
        )

    # Format system prompt with language version
    system_prompt = JAVA_SYSTEM_PROMPT_TEXT.format(language_version=f"Java {language_version}")

    # Build user prompt with line profiler results
    if is_multi_file:
        # For multi-file, identify the first file as the target and others as helper context
        file_paths = list(original_file_to_code.keys())
        target_file = file_paths[0] if file_paths else "main file"
        helper_files = file_paths[1:] if len(file_paths) > 1 else []

        # Build multi-file instructions
        helper_notice = ""
        if helper_files:
            helper_list = ", ".join(f"`{f}`" for f in helper_files)
            helper_notice = f"""
HELPER FILES: {helper_list}
These files contain helper classes/methods that the target code uses. You may optimize these as well if needed.
"""

        multi_file_instructions = f"""
The code is provided in a multi-file format. Each file is wrapped in a code block with its path.

TARGET FILE: `{target_file}`
{helper_notice}
Output the optimized code for each file that you modify. Wrap each file's code in:
```java:<file_path>
<optimized code>
```

You MUST output the target file. You may also output helper files if you optimize them.
"""
        system_prompt = system_prompt + "\n" + multi_file_instructions

        user_prompt = f"""Optimize the following Java code for better performance.

{JAVA_LINE_PROF_CONTEXT.format(line_profiler_results=line_profiler_results)}

Here is the code to optimize:
{source_code}
"""
    else:
        user_prompt = f"""Optimize the following Java code for better performance.

{JAVA_LINE_PROF_CONTEXT.format(line_profiler_results=line_profiler_results)}

Here is the code to optimize:
```java
{source_code}
```
"""

    if dependency_code:
        user_prompt = f"Dependencies (read-only):\n```java\n{dependency_code}\n```\n\n{user_prompt}"

    obs_context: dict = {}
    if call_sequence is not None:
        obs_context["call_sequence"] = call_sequence

    messages: list[ChatCompletionMessageParam] = [
        ChatCompletionSystemMessageParam(role="system", content=system_prompt),
        ChatCompletionUserMessageParam(role="user", content=user_prompt),
    ]

    try:
        output = await llm_client.call(
            llm=optimize_model,
            messages=messages,
            call_type="line_profiler",
            trace_id=trace_id,
            user_id=user_id,
            python_version=f"Java {language_version}",
            context=obs_context,
        )
    except Exception:
        debug_log_sensitive_data(f"Failed to generate code for source:\n{source_code}")
        return None, None, optimize_model.name

    llm_cost = output.cost

    debug_log_sensitive_data_from_callable(
        lambda: f"LLM optimization response:\n{output.raw_response.model_dump_json(indent=2)}"
    )

    if output.raw_response.usage is not None:
        ph(
            user_id,
            "aiservice-optimize-line-profiler-openai-usage",
            properties={"model": optimize_model.name, "usage": output.raw_response.usage.json(), "language": "java"},
        )

    # Extract code and explanation from response
    extracted_code, explanation = extract_java_code_and_explanation(output.content, is_multi_file=is_multi_file)

    if not extracted_code:
        sentry_sdk.capture_message("No code block found in Java line profiler optimization response")
        debug_log_sensitive_data(f"No code found in response for source:\n{source_code}")
        return None, llm_cost, optimize_model.name

    optimization_id = str(uuid.uuid4())

    if is_multi_file and isinstance(extracted_code, dict):
        # Handle multi-file response
        merged_file_to_code: dict[str, str] = {}
        has_changes = False

        for file_path, original_code in original_file_to_code.items():
            if file_path in extracted_code:
                new_code = extracted_code[file_path]

                # Validate the new code
                is_valid, error = validate_java_syntax(new_code)

                if not is_valid:
                    sentry_sdk.capture_message(f"Invalid Java generated for {file_path}: {error}")
                    debug_log_sensitive_data(f"Invalid code generated for {file_path}:\n{new_code}\nError: {error}")
                    # Keep original code for this file
                    merged_file_to_code[file_path] = original_code
                else:
                    merged_file_to_code[file_path] = new_code
                    if normalize_c_style_code(new_code) != normalize_c_style_code(original_code):
                        has_changes = True
            else:
                # File not in response, keep original
                merged_file_to_code[file_path] = original_code

        if not has_changes:
            debug_log_sensitive_data("Generated code identical to original (multi-file)")
            return None, llm_cost, optimize_model.name

        # Format as multi-file markdown
        wrapped_code = group_code(merged_file_to_code, language="java")

        result = OptimizeResponseItemSchema(
            source_code=wrapped_code, explanation=explanation, optimization_id=optimization_id
        )
        return result, llm_cost, optimize_model.name

    # Single file handling
    optimized_code = extracted_code if isinstance(extracted_code, str) else ""

    if not optimized_code:
        return None, llm_cost, optimize_model.name

    # Validate the generated code
    is_valid, error = validate_java_syntax(optimized_code)

    if not is_valid:
        sentry_sdk.capture_message(f"Invalid Java generated: {error}")
        debug_log_sensitive_data(f"Invalid code generated:\n{optimized_code}\nError: {error}")
        return None, llm_cost, optimize_model.name

    # Check that the code is actually different from the original
    if normalize_c_style_code(optimized_code) == normalize_c_style_code(source_code):
        debug_log_sensitive_data("Generated code identical to original")
        return None, llm_cost, optimize_model.name

    # Wrap code in markdown format for CLI parsing
    wrapped_code = (
        f"```java\n{optimized_code}\n```" if not optimized_code.endswith("\n") else f"```java\n{optimized_code}```"
    )
    result = OptimizeResponseItemSchema(
        source_code=wrapped_code, explanation=explanation, optimization_id=optimization_id
    )

    return result, llm_cost, optimize_model.name


async def optimize_java_code_line_profiler(
    user_id: str,
    trace_id: str,
    source_code: str,
    line_profiler_results: str,
    dependency_code: str | None = None,
    language_version: str = "17",
    n_candidates: int = 0,
) -> tuple[list[OptimizeResponseItemSchema], float, dict[str, str]]:
    """Run parallel Java line profiler optimizations with multiple models."""
    if n_candidates == 0:
        return [], 0.0, {}

    model_distribution = get_model_distribution(n_candidates, MAX_OPTIMIZER_LP_CALLS)
    tasks: list[asyncio.Task[tuple[OptimizeResponseItemSchema | None, float | None, str]]] = []
    call_sequence = 1

    async with asyncio.TaskGroup() as tg:
        for model, num_calls in model_distribution:
            for _ in range(num_calls):
                task = tg.create_task(
                    optimize_java_code_line_profiler_single(
                        user_id=user_id,
                        trace_id=trace_id,
                        source_code=source_code,
                        line_profiler_results=line_profiler_results,
                        dependency_code=dependency_code,
                        optimize_model=model,
                        language_version=language_version,
                        call_sequence=call_sequence,
                    )
                )
                tasks.append(task)
                call_sequence += 1

    # Collect results
    optimization_results: list[OptimizeResponseItemSchema] = []
    total_cost = 0.0
    optimization_models: dict[str, str] = {}

    for task in tasks:
        result, cost, model_name = task.result()
        if cost:
            total_cost += cost
        if result is not None:
            optimization_results.append(result)
            optimization_models[result.optimization_id] = model_name

    return optimization_results, total_cost, optimization_models
