"""Configuration parsing, version checking, and checkpoint management."""

from __future__ import annotations

import datetime
import json
import logging
import time
import uuid
from pathlib import Path
from typing import Any

import requests
import tomlkit
from packaging import version as pkg_version

from codeflash_python._compat import codeflash_temp_dir

from .. import __version__

log = logging.getLogger(__name__)

_version_cache: dict[str, Any] = {"version": None, "timestamp": 0}
_cache_duration: int = 3600  # 1 hour


def get_latest_version_from_pypi() -> str | None:
    """Get the latest version of codeflash from PyPI."""
    current_time = time.time()
    if (
        _version_cache["version"] is not None
        and current_time - _version_cache["timestamp"] < _cache_duration
    ):
        return str(_version_cache["version"])

    try:
        response = requests.get(
            "https://pypi.org/pypi/codeflash/json",
            timeout=2,
        )
        if response.status_code == 200:
            data = response.json()
            latest_version: str = data["info"]["version"]
            _version_cache["version"] = latest_version
            _version_cache["timestamp"] = current_time
            return latest_version
        log.debug(
            "Failed to fetch version from PyPI: %s",
            response.status_code,
        )
        return None
    except requests.RequestException:
        log.debug("Network error fetching version from PyPI")
        return None
    except (KeyError, ValueError):
        log.debug("Invalid response format from PyPI")
        return None
    except Exception:  # noqa: BLE001
        log.debug("Unexpected error fetching version from PyPI")
        return None


def check_for_newer_minor_version() -> None:
    """Warn if a newer version is available on PyPI."""
    latest_version = get_latest_version_from_pypi()
    if not latest_version:
        return

    try:
        current_parsed = pkg_version.parse(__version__)
        latest_parsed = pkg_version.parse(latest_version)

        if latest_parsed > current_parsed:
            log.warning(
                f"A newer version({latest_version}) of Codeflash"  # noqa: G004
                f" is available, please update soon!",
            )
    except pkg_version.InvalidVersion:
        log.debug("Invalid version format")


class CodeflashRunCheckpoint:
    """Track processed functions across optimization runs."""

    def __init__(
        self,
        module_root: Path,
        checkpoint_dir: Path | None = None,
    ) -> None:
        if checkpoint_dir is None:
            checkpoint_dir = codeflash_temp_dir
        self.module_root = module_root
        self.checkpoint_dir = Path(checkpoint_dir)
        unique_id = str(uuid.uuid4())[:8]
        checkpoint_filename = f"codeflash_checkpoint_{unique_id}.jsonl"
        self.checkpoint_path = self.checkpoint_dir / checkpoint_filename
        self._initialize_checkpoint_file()

    def _initialize_checkpoint_file(self) -> None:
        """Create a new checkpoint file with metadata."""
        metadata = {
            "type": "metadata",
            "module_root": str(self.module_root),
            "created_at": time.time(),
            "last_updated": time.time(),
        }
        with self.checkpoint_path.open("w", encoding="utf-8") as f:
            f.write(json.dumps(metadata) + "\n")

    def add_function_to_checkpoint(
        self,
        function_fully_qualified_name: str,
        status: str = "optimized",
        additional_info: dict[str, Any] | None = None,
    ) -> None:
        """Add a function to the checkpoint after processing."""
        if additional_info is None:
            additional_info = {}

        function_data = {
            "type": "function",
            "function_name": function_fully_qualified_name,
            "status": status,
            "timestamp": time.time(),
            **additional_info,
        }
        with self.checkpoint_path.open("a", encoding="utf-8") as f:
            f.write(json.dumps(function_data) + "\n")

        self._update_metadata_timestamp()

    def _update_metadata_timestamp(self) -> None:
        """Update the last_updated timestamp in the metadata."""
        with self.checkpoint_path.open(encoding="utf-8") as f:
            metadata = json.loads(f.readline())
            rest_content = f.read()

        metadata["last_updated"] = time.time()

        with self.checkpoint_path.open("w", encoding="utf-8") as f:
            f.write(json.dumps(metadata) + "\n")
            f.write(rest_content)

    def cleanup(self) -> None:
        """Unlink all checkpoint files for this module_root."""
        to_delete = []
        self.checkpoint_path.unlink(missing_ok=True)

        for file in self.checkpoint_dir.glob(
            "codeflash_checkpoint_*.jsonl",
        ):
            with file.open(encoding="utf-8") as f:
                first_line = next(f)
                metadata = json.loads(first_line)
                if metadata.get(
                    "module_root",
                    str(self.module_root),
                ) == str(self.module_root):
                    to_delete.append(file)
        for file in to_delete:
            file.unlink(missing_ok=True)


def get_all_historical_functions(
    module_root: Path,
    checkpoint_dir: Path,
) -> dict[str, dict[str, str]]:
    """Get information about all processed functions."""
    processed_functions: dict[str, dict[str, str]] = {}
    to_delete = []

    for file in checkpoint_dir.glob("codeflash_checkpoint_*.jsonl"):
        with file.open(encoding="utf-8") as f:
            first_line = next(f)
            metadata = json.loads(first_line)
            if metadata.get("last_updated"):
                last_updated = datetime.datetime.fromtimestamp(  # noqa: DTZ006
                    metadata["last_updated"],
                )
                if (
                    datetime.datetime.now()  # noqa: DTZ005
                    - last_updated
                    >= datetime.timedelta(days=7)
                ):
                    to_delete.append(file)
                    continue
            if metadata.get("module_root") != str(module_root):
                continue

            for line in f:
                entry = json.loads(line)
                if entry.get("type") == "function":
                    processed_functions[entry["function_name"]] = entry
    for file in to_delete:
        file.unlink(missing_ok=True)
    return processed_functions


def find_pyproject_toml(config_file: Path | None = None) -> Path:
    """Locate the pyproject.toml file."""
    if config_file is not None:
        config_file = Path(config_file)
        if config_file.suffix.lower() != ".toml":
            msg = f"Config file {config_file} is not a valid toml file."
            raise ValueError(msg)
        if not config_file.exists():
            msg = f"Config file {config_file} does not exist."
            raise ValueError(msg)
        return config_file
    dir_path = Path.cwd()
    while dir_path != dir_path.parent:
        candidate = dir_path / "pyproject.toml"
        if candidate.exists():
            return candidate
        dir_path = dir_path.parent
    msg = "Could not find a pyproject.toml file."
    raise FileNotFoundError(msg)


def parse_config_file(
    config_file_path: Path | None = None,
    *,
    override_formatter_check: bool = False,
) -> tuple[dict[str, Any], Path]:
    """Parse codeflash config from a pyproject.toml file."""
    config_file_path = find_pyproject_toml(config_file_path)
    try:
        with config_file_path.open("rb") as f:
            data = tomlkit.parse(f.read())
    except tomlkit.exceptions.ParseError as e:
        msg = (
            f"Error while parsing {config_file_path}."
            f" Please recheck for syntax errors. Error: {e}"
        )
        raise ValueError(msg) from None

    try:
        tool = data["tool"]
        if not isinstance(tool, dict):
            msg = f"Expected 'tool' to be a table in {config_file_path}."
            raise TypeError(msg)
        config = tool["codeflash"]
    except (tomlkit.exceptions.NonExistentKey, KeyError) as e:
        msg = f"Could not find the 'codeflash' block in {config_file_path}."
        raise ValueError(msg) from e
    if not isinstance(config, dict):
        msg = f"Expected 'codeflash' to be a table in {config_file_path}."
        raise TypeError(msg)
    cfg: dict[str, Any] = dict(config)

    path_keys = ["module-root", "tests-root", "benchmarks-root"]
    path_list_keys = ["ignore-paths"]
    str_keys: dict[str, str] = {
        "pytest-cmd": "pytest",
        "git-remote": "origin",
    }
    bool_keys: dict[str, bool] = {
        "override-fixtures": False,
        "disable-telemetry": False,
        "disable-imports-sorting": False,
        "benchmark": False,
    }
    list_str_keys: dict[str, list[str]] = {"formatter-cmds": []}

    for key, str_default in str_keys.items():
        cfg[key] = str(cfg[key]) if key in cfg else str_default
    for key, bool_default in bool_keys.items():
        cfg[key] = bool(cfg[key]) if key in cfg else bool_default
    for key in path_keys:
        if key in cfg:
            cfg[key] = str(
                (config_file_path.parent / Path(str(cfg[key]))).resolve()
            )
    for key, list_default in list_str_keys.items():
        if key in cfg:
            cfg[key] = [str(cmd) for cmd in cfg[key]]
        else:
            cfg[key] = list_default
    for key in path_list_keys:
        if key in cfg:
            cfg[key] = [
                str((config_file_path.parent / str(path)).resolve())
                for path in cfg[key]
            ]
        else:
            cfg[key] = []

    if (
        not override_formatter_check
        and cfg.get("formatter-cmds")
        and cfg["formatter-cmds"][0] == "your-formatter $file"
    ):
        msg = "The formatter command is not set correctly in pyproject.toml."
        raise ValueError(msg)

    for key in list(cfg.keys()):
        if "-" in key:
            cfg[key.replace("-", "_")] = cfg[key]
            del cfg[key]

    return cfg, config_file_path
