"""Async test result parsing from SQLite databases."""

from __future__ import annotations

import logging
import sqlite3
from typing import TYPE_CHECKING

from .._model import VerificationType
from ..test_discovery.models import TestType
from ._path_resolution import file_path_from_module_name
from .models import FunctionTestInvocation, InvocationId, TestResults

if TYPE_CHECKING:
    from pathlib import Path

    from ..benchmarking.models import ConcurrencyMetrics
    from .models import TestConfig, TestFiles

log = logging.getLogger(__name__)

_STDOUT_COLUMN_INDEX = 10

_BEHAVIOR_QUERY = (
    "SELECT test_module_path, test_class_name,"
    " test_function_name, function_getting_tested,"
    " loop_index, invocation_id, wall_time_ns,"
    " cpu_time_ns, return_value, verification_type, stdout"
    " FROM codeflash_results"
    " WHERE mode = 'behavior'"
)

_THROUGHPUT_QUERY = (
    "SELECT COUNT(*) FROM codeflash_results"
    " WHERE function_getting_tested = ?"
    " AND mode = 'performance'"
)

_CONCURRENCY_QUERY = (
    "SELECT sequential_time_ns, concurrent_time_ns,"
    " concurrency_factor"
    " FROM codeflash_results"
    " WHERE function_getting_tested = ?"
    " AND mode = 'concurrency'"
)


def parse_async_behavior_results(
    sqlite_file_path: Path,
    test_files: TestFiles,
    test_config: TestConfig,
) -> TestResults:
    """Parse behavior test results from the async results database."""
    test_results = TestResults()
    if not sqlite_file_path.exists():
        log.warning(
            "No async test results for %s found.",
            sqlite_file_path,
        )
        return test_results

    db: sqlite3.Connection | None = None
    try:
        db = sqlite3.connect(sqlite_file_path)
        cur = db.cursor()
        data = cur.execute(_BEHAVIOR_QUERY).fetchall()
    except (sqlite3.Error, OSError):
        log.warning(
            "Failed to parse async test results from %s.",
            sqlite_file_path,
            exc_info=True,
        )
        if db is not None:
            db.close()
        return test_results
    finally:
        if db is not None:
            db.close()

    for row in data:
        _process_behavior_row(row, test_files, test_config, test_results)

    return test_results


def _process_behavior_row(
    val: tuple[object, ...],
    test_files: TestFiles,
    test_config: TestConfig,
    test_results: TestResults,
) -> None:
    """Process a single behavior row from the async results table."""
    try:
        _process_behavior_row_inner(val, test_files, test_config, test_results)
    except Exception:
        log.exception("Failed to parse async behavior result")


def _process_behavior_row_inner(
    val: tuple[object, ...],
    test_files: TestFiles,
    test_config: TestConfig,
    test_results: TestResults,
) -> None:
    """Inner processing for a single async behavior row."""
    test_module_path = val[0]
    test_class_name = val[1] or None
    test_function_name = val[2] or None
    function_getting_tested = val[3]
    loop_index = val[4]
    invocation_id = val[5]
    wall_time_ns = val[6]
    cpu_time_ns = val[7]
    verification_type = val[9]
    stdout_text = (
        val[_STDOUT_COLUMN_INDEX] if len(val) > _STDOUT_COLUMN_INDEX else None
    )

    test_file_path = file_path_from_module_name(
        test_module_path,  # type: ignore[arg-type]
        test_config.tests_project_rootdir,
    )

    if verification_type in {
        VerificationType.INIT_STATE_FTO,
        VerificationType.INIT_STATE_HELPER,
    }:
        test_type: TestType = TestType.INIT_STATE_TEST
    else:
        found = test_files.get_test_type_by_original_file_path(
            test_file_path,
        )
        if found is None:
            found = test_files.get_test_type_by_instrumented_file_path(
                test_file_path,
            )
        if found is None:
            log.debug(
                "Skipping async result for %s: could not determine test type",
                test_function_name,
            )
            return
        test_type = found

    ret_val = None
    if loop_index == 1 and val[8]:
        import dill as pickle  # noqa: PLC0415

        try:
            ret_val = (pickle.loads(val[8]),)  # noqa: S301
        except Exception:  # noqa: BLE001
            log.debug(
                "Failed to deserialize return value for %s",
                test_function_name,
                exc_info=True,
            )
            return

    test_results.add(
        FunctionTestInvocation(
            loop_index=loop_index,  # type: ignore[arg-type]
            id=InvocationId(
                test_module_path=test_module_path,  # type: ignore[arg-type]
                test_class_name=test_class_name,  # type: ignore[arg-type]
                test_function_name=test_function_name,  # type: ignore[arg-type]
                function_getting_tested=function_getting_tested,  # type: ignore[arg-type]
                iteration_id=invocation_id,  # type: ignore[arg-type]
            ),
            file_name=test_file_path,
            did_pass=True,
            runtime=wall_time_ns,  # type: ignore[arg-type]
            test_framework=test_config.test_framework,
            test_type=test_type,
            return_value=ret_val,
            cpu_runtime=int(cpu_time_ns or 0),  # type: ignore[call-overload]
            timed_out=False,
            verification_type=(
                VerificationType(verification_type)
                if verification_type
                else None
            ),
            stdout=stdout_text or None,  # type: ignore[arg-type]
        ),
    )


def calculate_async_throughput(
    sqlite_file_path: Path,
    function_name: str,
) -> int:
    """Count completed async function executions from the results database."""
    if not sqlite_file_path.exists():
        return 0

    db: sqlite3.Connection | None = None
    try:
        db = sqlite3.connect(sqlite_file_path)
        cur = db.cursor()
        row = cur.execute(_THROUGHPUT_QUERY, (function_name,)).fetchone()
        return int(row[0]) if row else 0
    except (sqlite3.Error, OSError):
        log.warning(
            "Failed to read async throughput from %s.",
            sqlite_file_path,
            exc_info=True,
        )
        return 0
    finally:
        if db is not None:
            db.close()


def parse_async_concurrency_metrics(
    sqlite_file_path: Path,
    function_name: str,
) -> ConcurrencyMetrics | None:
    """Parse concurrency benchmark metrics from the results database."""
    if not sqlite_file_path.exists():
        return None

    db: sqlite3.Connection | None = None
    try:
        db = sqlite3.connect(sqlite_file_path)
        cur = db.cursor()
        rows = cur.execute(_CONCURRENCY_QUERY, (function_name,)).fetchall()
    except (sqlite3.Error, OSError):
        log.warning(
            "Failed to read async concurrency metrics from %s.",
            sqlite_file_path,
            exc_info=True,
        )
        if db is not None:
            db.close()
        return None
    finally:
        if db is not None:
            db.close()

    if not rows:
        return None

    total_seq = 0
    total_conc = 0
    factor = rows[0][2]
    for row in rows:
        total_seq += row[0]
        total_conc += row[1]

    count = len(rows)
    avg_seq = total_seq / count
    avg_conc = total_conc / count

    ratio = 1.0 if avg_conc == 0 else avg_seq / avg_conc

    from ..benchmarking.models import (  # noqa: PLC0415
        ConcurrencyMetrics,
    )

    return ConcurrencyMetrics(
        sequential_time_ns=int(avg_seq),
        concurrent_time_ns=int(avg_conc),
        concurrency_factor=factor,
        concurrency_ratio=ratio,
    )
