import json
import os
import re
from typing import Any, Callable, List
from .data import JsonPathExpression, SnippetData
from .result import AssertionResult, Op, OpResult
from pathlib import Path


class Check:
    def __init__(
        self,
        name: str,
        description: str | None = None,
        data: SnippetData | None = None
    ):
        self.name = name
        self.description = description

        if data is None:
            try:
                path = os.environ["LUNAR_BUNDLE_PATH"]
                data = SnippetData.from_file(Path(path))
            except KeyError:
                raise ValueError(
                    "LUNAR_BUNDLE_PATH is not set"
                )
            except ValueError as e:
                raise ValueError(
                    "invalid LUNAR_BUNDLE_PATH"
                ) from e
            except FileNotFoundError:
                raise ValueError(
                    f"LUNAR_BUNDLE_PATH does not exist: {path}"
                )

        if not isinstance(data, SnippetData):
            raise ValueError(
                f"Data must be a SnippetData instance, got {data}"
            )
        self.data = data

        self._accessed_paths = []
        self._used_vars = []
        self._results = []
        self._submitted = False

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.submit()

    def submit(self):
        if not self._submitted:
            output = {
                "name": self.name,
                "assertions": [
                    result.toJson()
                    for result in self._results
                ]
            }

            if len(self._accessed_paths) > 0:
                output["paths"] = list(dict.fromkeys(
                    p if p.startswith('.') else f'.{p}'
                    for p in self._accessed_paths
                ))

            if self.description is not None:
                output["description"] = self.description

            print(json.dumps(output), end="")

            self._submitted = True

    def _get_query_result(
        self,
        value: Any,
        all_instances: bool
    ) -> tuple[Any, list[str]]:
        if not isinstance(value, str) or not value.startswith("."):
            return value, []

        try:
            jsonPath = JsonPathExpression(value)
        except ValueError:
            return value, []

        if self.data is None:
            return None, []

        if all_instances:
            results = self.data.get_all(jsonPath)
            if not results:
                return None, []
            values = [r.value for r in results]
            paths = [p for r in results for p in r.paths]
            return values, paths
        else:
            result = self.data.get_merged(jsonPath)
            if result is None:
                return None, []
            return result.value, result.paths

    def _make_assertion(
        self,
        op: Op,
        check_fn: Callable[[Any], bool],
        value: Any,
        all_instances: bool,
        failure_message: str,
        can_report_no_data: bool = True
    ) -> AssertionResult:
        actual_value, paths = self._get_query_result(value, all_instances)
        self._accessed_paths.extend(paths)

        if actual_value is None and can_report_no_data:
            self._results.append(
                AssertionResult(
                    op=op,
                    args=[],
                    result=OpResult.NO_DATA,
                    failure_message=None
                )
            )
            return

        if all_instances:
            if not isinstance(actual_value, list):
                actual_value = [actual_value]
            ok = all(check_fn(v) for v in actual_value)
        else:
            ok = check_fn(actual_value)

        try:
            # We don't care about the actual value,
            # we just want to make sure its serializable at submission time
            serialized_value = str(actual_value)
        except Exception as e:
            type_name = type(actual_value).__name__
            serialized_value = f"<typename {type_name}: {e}>"

        self._results.append(
            AssertionResult(
                op=op,
                args=[serialized_value],
                result=OpResult.PASS if ok else OpResult.FAIL,
                failure_message=failure_message if not ok else None,
            )
        )

    def get(self, path: str) -> Any | None:
        try:
            jsonPath = JsonPathExpression(path)
        except ValueError as e:
            raise ValueError(f"Invalid JSON path: {path}") from e

        result = self.data.get_merged(jsonPath)
        if result is not None:
            self._accessed_paths.extend(result.paths)
            return result.value
        return None

    def get_all(self, path: str) -> List[Any]:
        try:
            jsonPath = JsonPathExpression(path)
        except ValueError as e:
            raise ValueError(f"Invalid JSON path: {path}") from e

        results = self.data.get_all(jsonPath)
        if results:
            self._accessed_paths.extend(p for r in results for p in r.paths)
            return [r.value for r in results]
        return []

    def assert_true(
        self,
        value: Any,
        failure_message: str | None = None,
        all_instances: bool = False
    ):
        self._make_assertion(
            Op.TRUE,
            lambda v: v is True,
            value,
            all_instances,
            failure_message or f"{value} is not true",
        )

    def assert_false(
        self,
        value: Any,
        failure_message: str | None = None,
        all_instances: bool = False
    ):
        self._make_assertion(
            Op.FALSE,
            lambda v: v is False,
            value,
            all_instances,
            failure_message or f"{value} is not false",
        )

    def assert_equals(
        self,
        value: Any,
        expected: Any,
        failure_message: str | None = None,
        all_instances: bool = False
    ):
        self._make_assertion(
            Op.EQUALS,
            lambda v: v == expected,
            value,
            all_instances,
            failure_message or f"{value} is not equal to {expected}",
        )

    def assert_contains(
        self,
        value: Any,
        expected: Any,
        failure_message: str | None = None,
        all_instances: bool = False
    ):
        self._make_assertion(
            Op.CONTAINS,
            lambda v: expected in v,
            value,
            all_instances,
            failure_message or f"{value} does not contain {expected}",
        )

    def assert_greater(
        self,
        value: Any,
        expected: Any,
        failure_message: str | None = None,
        all_instances: bool = False
    ):
        self._make_assertion(
            Op.GREATER,
            lambda v: v > expected,
            value,
            all_instances,
            failure_message or f"{value} is not greater than {expected}",
        )

    def assert_greater_or_equal(
        self,
        value: Any,
        expected: Any,
        failure_message: str | None = None,
        all_instances: bool = False
    ):
        self._make_assertion(
            Op.GREATER_OR_EQUAL,
            lambda v: v >= expected,
            value,
            all_instances,
            failure_message or
            f"{value} is not greater than or equal to {expected}",
        )

    def assert_less(
        self,
        value: Any,
        expected: Any,
        failure_message: str | None = None,
        all_instances: bool = False
    ):
        self._make_assertion(
            Op.LESS,
            lambda v: v < expected,
            value,
            all_instances,
            failure_message or f"{value} is not less than {expected}",
        )

    def assert_less_or_equal(
        self,
        value: Any,
        expected: Any,
        failure_message: str | None = None,
        all_instances: bool = False
    ):
        self._make_assertion(
            Op.LESS_OR_EQUAL,
            lambda v: v <= expected,
            value,
            all_instances,
            failure_message or
            f"{value} is not less than or equal to {expected}",
        )

    def assert_match(
        self,
        value: Any,
        pattern: str,
        failure_message: str | None = None,
        all_instances: bool = False
    ):
        self._make_assertion(
            Op.MATCH,
            lambda v: re.match(pattern, v) is not None,
            value,
            all_instances,
            failure_message or f"{value} does not match {pattern}",
        )

    def assert_exists(
        self,
        value: Any,
        failure_message: str | None = None
    ):
        self._make_assertion(
            Op.EXISTS,
            lambda v: v is not None,
            value,
            False,
            failure_message or "value does not exist at path",
            can_report_no_data=False
        )

    def assert_missing(
        self,
        value: Any,
        failure_message: str | None = None
    ):
        self._make_assertion(
            Op.MISSING,
            lambda v: v is None,
            value,
            False,
            failure_message or "value exists at path",
            can_report_no_data=False
        )
