# Author: Koushik Sen (ksen@berkeley.edu)
# Contributors:
# Koushik Sen (ksen@berkeley.edu)
# add your name here

"""Utility functions for the KISS core module."""

import logging
from pathlib import Path
from string import Formatter as StringFormatter
from typing import Any, cast

import yaml

from kiss.core import config as config_module
from kiss.core.kiss_error import KISSError

logger = logging.getLogger(__name__)

def get_config_value[T](
    value: T | None, config_obj: Any, attr_name: str, default: T | None = None
) -> T:
    """Get a config value, preferring explicit value over config default.

    This eliminates the repetitive pattern:
        value if value is not None else config.attr_name

    Args:
        value: The explicitly provided value (may be None)
        config_obj: The config object to read from if value is None
        attr_name: The attribute name to read from config_obj
        default: Fallback default if both value and config attribute are None

    Returns:
        The resolved value (explicit value > config value > default)
    """
    if value is not None:
        return value
    config_value = getattr(config_obj, attr_name, None)
    if config_value is not None:
        return cast(T, config_value)
    if default is not None:
        return default
    raise ValueError(f"No value provided and config.{attr_name} is not set")


def get_template_field_names(text: str) -> list[str]:
    """Get the field names from the text.

    Args:
        text (str): The text containing template field placeholders.

    Returns:
        list[str]: A list of field names found in the text.
    """
    return [
        field_name
        for _, field_name, _, _ in StringFormatter().parse(text)
        if field_name is not None
    ]


def add_prefix_to_each_line(text: str, prefix: str) -> str:
    """Adds a prefix to each line of the text.

    Args:
        text (str): The text to add prefix to.
        prefix (str): The prefix to add to each line.

    Returns:
        str: The text with prefix added to each line.
    """
    return "\n".join([f"{prefix}{line}" for line in text.split("\n")])


def config_to_dict() -> dict[Any, Any]:
    """Convert the config to a dictionary.

    Returns:
        dict[Any, Any]: A dictionary representation of the default config.
    """

    def convert_to_json(obj: Any) -> Any:
        if isinstance(obj, dict):
            return {k: convert_to_json(v) for k, v in obj.items() if "API_KEY" not in k}  # type: ignore[misc]
        if isinstance(obj, list):
            return [convert_to_json(item) for item in obj]  # type: ignore[misc]
        if isinstance(obj, (str, int, float, bool, type(None))):
            return obj
        if hasattr(obj, "__dict__"):
            return {
                k: convert_to_json(getattr(obj, k))
                for k in obj.__dict__.keys()
                if "API_KEY" not in k
            }
        return obj

    return cast(dict[Any, Any], convert_to_json(config_module.DEFAULT_CONFIG))


def fc(file_path: str) -> str:
    """Reads a file and returns the content.

    Args:
        file_path (str): The path to the file to read.

    Returns:
        str: The content of the file.
    """
    return Path(file_path).read_text()


def finish(
    status: str = "success",
    analysis: str = "",
    result: str = "",
) -> str:
    """
    The agent must call this function with the final status, analysis, and result
    when it has solved the given task. Status **MUST** be 'success' or 'failure'.

    Args:
        status: The status of the agent's task ('success' or 'failure'). Defaults to 'success'.
        analysis: The analysis of the agent's trajectory.
        result: The result generated by the agent.

    Returns:
        A YAML string containing the status, analysis, and result of the agent's task.
    """
    return str(
        yaml.dump(
            {
                "status": status,
                "analysis": analysis,
                "result": result,
            },
            indent=2,
            sort_keys=False,
        )
    )


def read_project_file(file_path_relative_to_project_root: str) -> str:
    """Read a file from the project root.

    Compatible with installations packaged as .whl (zip) or source.

    Args:
        file_path_relative_to_project_root (str): Path relative to the project root.

    Returns:
        str: The file's contents.
    """
    import importlib.resources
    import os

    # Try usual filesystem access from root
    project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
    abs_path = os.path.join(project_root, file_path_relative_to_project_root)
    if os.path.isfile(abs_path):
        with open(abs_path, encoding="utf-8") as f:
            return f.read()

    rel_parts = file_path_relative_to_project_root.strip("/").split("/")
    if len(rel_parts) > 1:
        pkg = ".".join(rel_parts[:-1])
        file = rel_parts[-1]
    else:
        pkg = ""
        file = file_path_relative_to_project_root

    try:
        if pkg:
            return importlib.resources.read_text(pkg, file, encoding="utf-8")
        else:
            # If no package, try relative to this module's package
            package = __package__ or "kiss.core"
            return importlib.resources.read_text(package, file, encoding="utf-8")
    except Exception as e:
        logger.debug("Exception caught", exc_info=True)
        raise KISSError(
            f"Could not find '{file_path_relative_to_project_root}' "
            f"as a file or in a package. ({e})"
        )


def read_project_file_from_package(file_name_as_python_package: str) -> str:
    """Read a file from the project root.

    Args:
        file_name_as_python_package (str): File name as a Python package.

    Returns:
        str: The file's contents.
    """
    import importlib.resources

    try:
        package = __package__ or "kiss.core"
        return importlib.resources.read_text(package, file_name_as_python_package, encoding="utf-8")
    except Exception as e:
        logger.debug("Exception caught", exc_info=True)
        raise KISSError(
            f"Could not find '{file_name_as_python_package}' as a file or in a package. ({e})"
        )


def resolve_path(p: str, base_dir: str) -> Path:
    """Resolve a path relative to base_dir if not absolute.

    Args:
        p: The path string to resolve.
        base_dir: The base directory for relative path resolution.

    Returns:
        Path: The resolved absolute path.
    """
    path = Path(p)
    if not path.is_absolute():
        return (Path(base_dir) / path).resolve()
    return path.resolve()


def is_subpath(target: Path, whitelist: list[Path]) -> bool:
    """Check if target has any prefix in whitelist.

    Args:
        target: The path to check.
        whitelist: List of allowed path prefixes.

    Returns:
        bool: True if target is under any path in whitelist, False otherwise.
    """
    target = Path(target).resolve()
    return any(target.is_relative_to(p) for p in whitelist)
