"""Capability protocols for language-specific callables.

These protocols formalize the function signatures that core
pipeline steps already accept as parameters.  Instead of
documenting "pass a callable that normalizes code" in prose,
we now have typed contracts.

Language packages implement these protocols (structurally —
no inheritance needed) and declare them in their
:class:`LanguagePlugin`.  Core pipeline functions type their
parameters against these protocols for static checking.

This is the language equivalent of cloud abstractions'
``AbstractComponent`` subclasses (VPC, ObjectStore, etc.) —
each protocol defines what one capability looks like across
all languages.

Example usage in a core pipeline function::

    from codeflash_core import NormalizeFn

    def dedup_candidates(
        candidates: list[Candidate],
        *,
        normalize_fn: NormalizeFn,
        ...
    ) -> list[Candidate]:
        ...

Example implementation in a language package::

    # codeflash_python/analysis/_normalizer.py
    def normalize_python_code(code: str) -> str:
        ...  # satisfies NormalizeFn structurally
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Protocol, runtime_checkable

if TYPE_CHECKING:
    from pathlib import Path


# -- Normalization -------------------------------------------------


@runtime_checkable
class NormalizeFn(Protocol):
    """Normalize source code for deduplication comparison.

    Takes raw source code and returns a canonical form where
    semantically equivalent code produces identical strings.
    Used by :func:`dedup_candidates` to detect duplicate
    optimization candidates.

    Python: strips comments, normalizes whitespace, sorts imports.
    JavaScript: might normalize semicolons, quote styles, etc.
    """

    def __call__(self, code: str) -> str: ...


# -- Discovery ----------------------------------------------------


@runtime_checkable
class DiscoverFn(Protocol):
    """Discover optimizable functions in a source file.

    Takes the source text and its file path, returns a list of
    function descriptors.  The return type is ``list[object]``
    because function models are language-specific (Python has
    ``FunctionToOptimize``, JavaScript will have its own).

    Each returned object must have at least:
    - ``function_name: str``
    - ``qualified_name: str``
    - ``file_path: Path``
    """

    def __call__(
        self,
        source: str,
        file_path: Path,
    ) -> list[object]: ...


# -- Context Extraction --------------------------------------------


@runtime_checkable
class ExtractContextFn(Protocol):
    """Extract optimization context for a function.

    Gathers the surrounding code context that the AI service
    needs to generate good optimizations: imports, class
    definitions, helper functions, type annotations, etc.

    The return type is ``object`` because context models are
    language-specific.  Each returned object must have at least:
    - ``read_writable: str`` — code the AI can modify
    - ``read_only: str`` — surrounding context for reference
    """

    def __call__(
        self,
        function: object,
        project_root: Path,
    ) -> object: ...


# -- Code Replacement ----------------------------------------------


@runtime_checkable
class ReplaceCodeFn(Protocol):
    """Replace function definitions in source code.

    Takes the original source, a list of function names to
    replace, and the new optimized code.  Returns the updated
    source with the functions replaced.

    Language packages handle the AST/CST manipulation
    internally (Python uses libcst, JavaScript might use
    babel/recast).
    """

    def __call__(
        self,
        source_code: str,
        function_names: list[str],
        optimized_code: str,
    ) -> str: ...


# -- Test Execution ------------------------------------------------


@runtime_checkable
class RunTestsFn(Protocol):
    """Run behavioral tests and return raw results.

    Executes the test suite (or a subset) against the current
    code and returns a results object.  The return type is
    ``object`` because test result models are language-specific.

    Each returned object must have at least:
    - ``passed: bool``
    - ``runtime_ns: int``
    """

    def __call__(
        self,
        test_files: object,
        cwd: Path,
    ) -> object: ...


@runtime_checkable
class ParseResultsFn(Protocol):
    """Parse raw test output into structured results.

    Takes the raw output from the test runner and returns
    a structured results object.  Separated from
    :class:`RunTestsFn` so that the same parser can be used
    for cached/replayed results.
    """

    def __call__(self, raw_output: object) -> object: ...


@runtime_checkable
class CompareResultsFn(Protocol):
    """Compare original and optimized test results.

    Returns ``True`` if the optimized code is behaviorally
    equivalent to the original (same outputs, no regressions).
    """

    def __call__(
        self,
        original: object,
        optimized: object,
    ) -> bool: ...


# -- Capability Map ------------------------------------------------


REQUIRED_CAPABILITIES: frozenset[str] = frozenset(
    {
        "normalize_code",
        "discover_functions",
        "extract_context",
        "replace_code",
        "run_tests",
        "parse_results",
        "compare_results",
    },
)
"""Capability names that every language plugin must provide.

Used by :func:`validate_capabilities` to check that a plugin
declares all required callables.
"""

OPTIONAL_CAPABILITIES: frozenset[str] = frozenset(
    {
        "run_benchmarks",
        "generate_tests",
        "detect_numerical",
    },
)
"""Capability names that are useful but not required."""


def validate_capabilities(
    capabilities: dict[str, object],
) -> list[str]:
    """Return a list of missing required capabilities.

    Returns an empty list if all required capabilities are
    present.  Callers can use this at plugin construction
    time to fail fast with a clear error message.
    """
    return sorted(REQUIRED_CAPABILITIES - capabilities.keys())
