"""Core data models for functions and optimization targets."""

from __future__ import annotations

import enum
import sys
from pathlib import Path

import attrs

if sys.version_info >= (3, 11):
    from typing import Self
else:
    from typing_extensions import Self


@attrs.frozen
class FunctionParent:
    """A parent scope (class or function) enclosing a target function."""

    name: str
    type: str

    def __str__(self) -> str:
        """Return type:name representation."""
        return f"{self.type}:{self.name}"

    def to_dict(self) -> dict[str, str]:
        """Serialize to a plain dictionary."""
        return {"name": self.name, "type": self.type}

    @classmethod
    def from_dict(cls, data: dict[str, str]) -> Self:
        """Restore from a serialized dictionary."""
        return cls(name=data["name"], type=data["type"])


@attrs.frozen
class FunctionToOptimize:
    """A Python function targeted for optimization."""

    function_name: str
    file_path: Path = attrs.field(converter=Path)
    parents: tuple[FunctionParent, ...] = ()
    starting_line: int | None = None
    ending_line: int | None = None
    starting_col: int | None = None
    ending_col: int | None = None
    is_async: bool = False
    is_method: bool = False
    doc_start_line: int | None = None

    @property
    def qualified_name(self) -> str:
        """Dotted name including parent classes/functions."""
        parts = [p.name for p in self.parents]
        parts.append(self.function_name)
        return ".".join(parts)

    @property
    def top_level_parent_name(self) -> str:
        """Name of the outermost parent, or the function itself."""
        if not self.parents:
            return self.function_name
        return self.parents[0].name

    @property
    def class_name(self) -> str | None:
        """Name of the nearest enclosing class, or *None*."""
        for parent in reversed(self.parents):
            if parent.type == "ClassDef":
                return parent.name
        return None

    def qualified_name_with_modules_from_root(
        self,
        project_root_path: Path,
    ) -> str:
        """Fully qualified dotted name from the project root."""
        from .test_discovery.linking import (  # noqa: PLC0415
            module_name_from_file_path,
        )

        module = module_name_from_file_path(self.file_path, project_root_path)
        return f"{module}.{self.qualified_name}"

    def to_dict(self) -> dict[str, object]:
        """Serialize to a plain dictionary."""
        return {
            "function_name": self.function_name,
            "file_path": str(self.file_path),
            "parents": [p.to_dict() for p in self.parents],
            "starting_line": self.starting_line,
            "ending_line": self.ending_line,
            "starting_col": self.starting_col,
            "ending_col": self.ending_col,
            "is_async": self.is_async,
            "is_method": self.is_method,
            "doc_start_line": self.doc_start_line,
        }

    @classmethod
    def from_dict(
        cls,
        data: dict[str, object],
    ) -> Self:
        """Restore from a serialized dictionary."""
        parents_raw: list[dict[str, str]] = data.get(  # type: ignore[assignment]
            "parents",
            [],
        )
        parents = tuple(FunctionParent.from_dict(p) for p in parents_raw)

        def _opt_int(val: object) -> int | None:
            return int(str(val)) if val is not None else None

        return cls(
            function_name=str(data["function_name"]),
            file_path=Path(str(data["file_path"])),
            parents=parents,
            starting_line=_opt_int(data.get("starting_line")),
            ending_line=_opt_int(data.get("ending_line")),
            starting_col=_opt_int(data.get("starting_col")),
            ending_col=_opt_int(data.get("ending_col")),
            is_async=bool(data.get("is_async", False)),
            is_method=bool(data.get("is_method", False)),
            doc_start_line=_opt_int(data.get("doc_start_line")),
        )


class TestingMode(enum.Enum):
    """Mode for test instrumentation."""

    __test__ = False

    BEHAVIOR = "behavior"
    PERFORMANCE = "performance"
    LINE_PROFILE = "line_profile"
    CONCURRENCY = "concurrency"


class VerificationType(str, enum.Enum):
    """Type of correctness verification."""

    FUNCTION_CALL = "function_call"
    INIT_STATE_FTO = "init_state_fto"
    INIT_STATE_HELPER = "init_state_helper"


@attrs.frozen
class FunctionSource:
    """A resolved Python function with its fully-qualified name."""

    file_path: Path = attrs.field(converter=Path)
    qualified_name: str
    fully_qualified_name: str
    source_code: str
    only_function_name: str | None = None
    definition_type: str | None = None

    def to_dict(self) -> dict[str, object]:
        """Serialize to a plain dictionary."""
        return {
            "file_path": str(self.file_path),
            "qualified_name": self.qualified_name,
            "fully_qualified_name": self.fully_qualified_name,
            "source_code": self.source_code,
            "only_function_name": self.only_function_name,
            "definition_type": self.definition_type,
        }

    @classmethod
    def from_dict(cls, data: dict[str, object]) -> Self:
        """Restore from a serialized dictionary."""
        ofn = data.get("only_function_name")
        dt = data.get("definition_type")
        return cls(
            file_path=Path(str(data["file_path"])),
            qualified_name=str(data["qualified_name"]),
            fully_qualified_name=str(data["fully_qualified_name"]),
            source_code=str(data["source_code"]),
            only_function_name=str(ofn) if ofn is not None else None,
            definition_type=str(dt) if dt is not None else None,
        )
