"""Utility functions for correlation IDs, response formatting and query building."""

import json
import logging
import re
import uuid
import warnings
from collections.abc import Awaitable, Callable
from typing import Any

from servicenow_mcp.errors import ForbiddenError

logger = logging.getLogger(__name__)

_IDENTIFIER_RE = re.compile(r"^[a-z0-9_]+$")

# Operators recognised by ``or_condition()``.
_ALLOWED_OPERATORS: frozenset[str] = frozenset(
    {
        "=",
        "!=",
        ">",
        ">=",
        "<",
        "<=",
        "CONTAINS",
        "STARTSWITH",
        "LIKE",
        "ISEMPTY",
        "ISNOTEMPTY",
        "IN",
        "NOT IN",
    }
)


def validate_identifier(name: str) -> None:
    """Raise ValueError if *name* is not a valid ServiceNow identifier.

    ServiceNow table and field names consist only of lowercase
    alphanumerics and underscores (``[a-z0-9_]+``).
    """
    if not _IDENTIFIER_RE.match(name):
        raise ValueError(
            f"Invalid identifier: {name!r}. Only lowercase alphanumeric characters and underscores are allowed."
        )


def sanitize_query_value(value: str) -> str:
    """Escape special encoded-query delimiters in a user-supplied value.

    ServiceNow uses ``^`` as the condition separator in encoded queries.
    A literal caret inside a *value* is represented as ``^^``.
    """
    return value.replace("^", "^^")


def generate_correlation_id() -> str:
    """Generate a unique correlation ID for request tracing."""
    return str(uuid.uuid4())


def format_response(
    data: Any,
    correlation_id: str,
    status: str = "success",
    error: str | None = None,
    pagination: dict[str, int] | None = None,
    warnings: list[str] | None = None,
) -> dict[str, Any]:
    """Build a standardized response envelope."""
    response: dict[str, Any] = {
        "correlation_id": correlation_id,
        "status": status,
        "data": data,
    }
    if error is not None:
        response["error"] = error
    if pagination is not None:
        response["pagination"] = pagination
    if warnings is not None:
        response["warnings"] = warnings
    return response


def build_encoded_query(conditions: dict[str, str] | str) -> str:
    """Convert a dict of conditions to a ServiceNow encoded query string.

    If a string is passed, it is returned unchanged.

    .. deprecated::
        Use :class:`ServiceNowQuery` instead.  This helper performs no
        field-name validation and only basic value sanitization.
    """
    warnings.warn(
        "build_encoded_query() is deprecated — use ServiceNowQuery instead.",
        DeprecationWarning,
        stacklevel=2,
    )
    if isinstance(conditions, str):
        return conditions
    if not conditions:
        return ""
    return "^".join(f"{key}={sanitize_query_value(value)}" for key, value in conditions.items())


class ServiceNowQuery:
    """Fluent builder for ServiceNow encoded query strings."""

    def __init__(self) -> None:
        self._parts: list[str] = []

    # --- Comparison operators ---

    def equals(self, field: str, value: str) -> "ServiceNowQuery":
        """Add ``field=value`` condition."""
        validate_identifier(field)
        value = sanitize_query_value(value)
        self._parts.append(f"{field}={value}")
        return self

    def not_equals(self, field: str, value: str) -> "ServiceNowQuery":
        """Add ``field!=value`` condition."""
        validate_identifier(field)
        value = sanitize_query_value(value)
        self._parts.append(f"{field}!={value}")
        return self

    def greater_than(self, field: str, value: str) -> "ServiceNowQuery":
        """Add ``field>value`` condition."""
        validate_identifier(field)
        value = sanitize_query_value(value)
        self._parts.append(f"{field}>{value}")
        return self

    def greater_or_equal(self, field: str, value: str) -> "ServiceNowQuery":
        """Add ``field>=value`` condition."""
        validate_identifier(field)
        value = sanitize_query_value(value)
        self._parts.append(f"{field}>={value}")
        return self

    def less_than(self, field: str, value: str) -> "ServiceNowQuery":
        """Add ``field<value`` condition."""
        validate_identifier(field)
        value = sanitize_query_value(value)
        self._parts.append(f"{field}<{value}")
        return self

    def less_or_equal(self, field: str, value: str) -> "ServiceNowQuery":
        """Add ``field<=value`` condition."""
        validate_identifier(field)
        value = sanitize_query_value(value)
        self._parts.append(f"{field}<={value}")
        return self

    # --- String operators ---

    def contains(self, field: str, value: str) -> "ServiceNowQuery":
        """Add ``fieldCONTAINSvalue`` condition."""
        validate_identifier(field)
        value = sanitize_query_value(value)
        self._parts.append(f"{field}CONTAINS{value}")
        return self

    def starts_with(self, field: str, value: str) -> "ServiceNowQuery":
        """Add ``fieldSTARTSWITHvalue`` condition."""
        validate_identifier(field)
        value = sanitize_query_value(value)
        self._parts.append(f"{field}STARTSWITH{value}")
        return self

    def like(self, field: str, value: str) -> "ServiceNowQuery":
        """Add ``fieldLIKEvalue`` condition."""
        validate_identifier(field)
        value = sanitize_query_value(value)
        self._parts.append(f"{field}LIKE{value}")
        return self

    # --- Null operators ---

    def is_empty(self, field: str) -> "ServiceNowQuery":
        """Add ``fieldISEMPTY`` condition."""
        validate_identifier(field)
        self._parts.append(f"{field}ISEMPTY")
        return self

    def is_not_empty(self, field: str) -> "ServiceNowQuery":
        """Add ``fieldISNOTEMPTY`` condition."""
        validate_identifier(field)
        self._parts.append(f"{field}ISNOTEMPTY")
        return self

    # --- GlideSystem time filters (server-side, timezone-correct) ---

    def hours_ago(self, field: str, hours: int) -> "ServiceNowQuery":
        """Add ``field>=javascript:gs.hoursAgoStart(hours)`` condition.

        Args:
            field: Field name (validated as identifier).
            hours: Number of hours, 1-8760 (1 year).
        """
        validate_identifier(field)
        hours = int(hours)
        if not (1 <= hours <= 8760):
            raise ValueError(f"hours must be between 1 and 8760, got {hours}")
        self._parts.append(f"{field}>=javascript:gs.hoursAgoStart({hours})")
        return self

    def minutes_ago(self, field: str, minutes: int) -> "ServiceNowQuery":
        """Add ``field>=javascript:gs.minutesAgoStart(minutes)`` condition.

        Args:
            field: Field name (validated as identifier).
            minutes: Number of minutes, 1-525600 (1 year).
        """
        validate_identifier(field)
        minutes = int(minutes)
        if not (1 <= minutes <= 525600):
            raise ValueError(f"minutes must be between 1 and 525600, got {minutes}")
        self._parts.append(f"{field}>=javascript:gs.minutesAgoStart({minutes})")
        return self

    def days_ago(self, field: str, days: int) -> "ServiceNowQuery":
        """Add ``field>=javascript:gs.daysAgoStart(days)`` condition.

        Args:
            field: Field name (validated as identifier).
            days: Number of days, 1-365.
        """
        validate_identifier(field)
        days = int(days)
        if not (1 <= days <= 365):
            raise ValueError(f"days must be between 1 and 365, got {days}")
        self._parts.append(f"{field}>=javascript:gs.daysAgoStart({days})")
        return self

    def older_than_days(self, field: str, days: int) -> "ServiceNowQuery":
        """Add ``field<=javascript:gs.daysAgoEnd(days)`` condition for records before the cutoff.

        Args:
            field: Field name (validated as identifier).
            days: Number of days, 1-3650 (10 years).
        """
        validate_identifier(field)
        days = int(days)
        if not (1 <= days <= 3650):
            raise ValueError(f"days must be between 1 and 3650, got {days}")
        self._parts.append(f"{field}<=javascript:gs.daysAgoEnd({days})")
        return self

    # --- IN / NOT IN operators ---

    def in_list(self, field: str, values: list[str]) -> "ServiceNowQuery":
        """Add ``fieldINvalue1,value2,...`` condition.

        Args:
            field: Field name (validated as identifier).
            values: List of values; each is sanitized individually.
        """
        validate_identifier(field)
        sanitized = ",".join(sanitize_query_value(v) for v in values)
        self._parts.append(f"{field}IN{sanitized}")
        return self

    def not_in_list(self, field: str, values: list[str]) -> "ServiceNowQuery":
        """Add ``fieldNOT INvalue1,value2,...`` condition.

        Args:
            field: Field name (validated as identifier).
            values: List of values; each is sanitized individually.
        """
        validate_identifier(field)
        sanitized = ",".join(sanitize_query_value(v) for v in values)
        self._parts.append(f"{field}NOT IN{sanitized}")
        return self

    # --- OR conditions ---

    def or_condition(self, field: str, operator: str, value: str) -> "ServiceNowQuery":
        """Append an OR condition: ``^ORfield<OPERATOR>value``.

        Args:
            field: Field name (validated as identifier).
            operator: One of the allowed ServiceNow operators (e.g. ``=``, ``!=``, ``CONTAINS``).
            value: Condition value (sanitized).
        """
        validate_identifier(field)
        if operator not in _ALLOWED_OPERATORS:
            raise ValueError(f"Unknown operator: {operator!r}. Allowed: {sorted(_ALLOWED_OPERATORS)}")
        value = sanitize_query_value(value)
        self._parts.append(f"OR{field}{operator}{value}")
        return self

    def or_equals(self, field: str, value: str) -> "ServiceNowQuery":
        """Convenience: append ``^ORfield=value``.

        Args:
            field: Field name (validated as identifier).
            value: Condition value (sanitized).
        """
        return self.or_condition(field, "=", value)

    def or_starts_with(self, field: str, value: str) -> "ServiceNowQuery":
        """Convenience: append ``^ORfieldSTARTSWITHvalue``.

        Args:
            field: Field name (validated as identifier).
            value: Condition value (sanitized).
        """
        return self.or_condition(field, "STARTSWITH", value)

    # --- Ordering ---

    def order_by(self, field: str, descending: bool = False) -> "ServiceNowQuery":
        """Append an ``ORDERBY`` or ``ORDERBYDESC`` directive.

        Args:
            field: Field name (validated as identifier).
            descending: If ``True`` use ``ORDERBYDESC``, otherwise ``ORDERBY``.
        """
        validate_identifier(field)
        prefix = "ORDERBYDESC" if descending else "ORDERBY"
        self._parts.append(f"{prefix}{field}")
        return self

    # --- Raw fragment ---

    def raw(self, fragment: str) -> "ServiceNowQuery":
        """Append a raw encoded query fragment.

        .. warning::

            This method performs **no** validation or sanitization.
            Only use it for trusted, pre-validated query fragments.
            Prefer the typed builder methods whenever possible to
            ensure field-name validation and value escaping.
        """
        if fragment:
            self._parts.append(fragment)
        return self

    # --- Build ---

    def build(self) -> str:
        """Return the joined encoded query string."""
        return "^".join(self._parts)

    def __str__(self) -> str:
        """Return the built query string."""
        return self.build()


async def safe_tool_call(
    fn: Callable[[], Awaitable[str]],
    correlation_id: str,
) -> str:
    """Wrap an MCP tool body with standard error handling.

    Catches ForbiddenError (ACL denial) and generic exceptions,
    returning consistent JSON error envelopes via format_response.
    """
    try:
        return await fn()
    except ForbiddenError as e:
        return json.dumps(
            format_response(
                data=None,
                correlation_id=correlation_id,
                status="error",
                error=f"Access denied by ServiceNow ACL: {e}",
            )
        )
    except Exception as e:
        return json.dumps(
            format_response(
                data=None,
                correlation_id=correlation_id,
                status="error",
                error=str(e),
            )
        )
