# dialects/oracle/json.py
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
# the MIT License: https://www.opensource.org/licenses/mit-license.php
# mypy: ignore-errors

from __future__ import annotations

from typing import Any
from typing import TYPE_CHECKING
from typing import TypeVar

from ... import types as sqltypes
from ...sql.base import _NoArg
from ...sql.base import NO_ARG
from ...sql.sqltypes import _T_JSON

if TYPE_CHECKING:
    from ...engine.interfaces import Dialect
    from ...sql.type_api import _BindProcessorType
    from ...sql.type_api import _LiteralProcessorType

_T = TypeVar("_T", bound=Any)


class JSON(sqltypes.JSON[_T_JSON]):
    """Oracle JSON type.

    .. versionadded:: 2.1

    Oracle Database supports JSON storage and querying for character and BLOB
    datatypes in Oracle 12c, and supports a dedicated JSON data type as of
    Oracle 21c. SQLAlchemy supports both of these scenarios when using the
    oracledb DBAPI.  This type is used implicitly whenever the base
    :class:`_types.JSON` datatype is used against an Oracle backend, or may be
    constructed directly for access to Oracle-specific parameters such as
    :paramref:`_oracle.JSON.use_blob`.

    Index operations are adapted to render using the ``JSON_QUERY`` and
    ``JSON_VALUE`` functions at the database level.

    **Platform Support** - When using Oracle Database versions prior to 21c,
    BLOB is used as the storage format.  In 21c or later, the native JSON
    datatype is used.  This can be overridden using the
    :paramref:`_oracle.JSON.use_blob` parameter.

    **Serialization / Deserialization** - JSON serialization of bound
    parameters uses Python ``json.dumps()`` by default rather than oracledb's
    native serializer, in order to support the
    :paramref:`_sqltypes.JSON.none_as_null` feature.  The default serializer
    does **not** accept Python ``Decimal`` objects; to use a custom serializer,
    pass :paramref:`_sa.create_engine.json_serializer` to
    :func:`_sa.create_engine`.

    When using the native JSON datatype (21c+), deserialization uses oracledb's
    native deserializer by default, which is required for JSON values larger
    than 32767 bytes.  However, this deserializer returns all numeric values as
    ``Decimal`` since Oracle Database stores JSON numbers using its internal
    NUMBER type.  To receive standard Python numeric types, pass
    ``json_deserializer=json.loads`` via
    :paramref:`_sa.create_engine.json_deserializer`; note that this limits
    maximum JSON value size to 32767 bytes.  When using BLOB storage,
    SQLAlchemy deserializes using ``json.loads()`` directly rather than the
    oracledb deserializer.

    **CHECK Constraint with BLOB** - When using BLOB storage, either on Oracle
    Database versions prior to 21c or via the :paramref:`_oracle.JSON.use_blob`
    parameter, the oracledb driver documentation recommends adding a
    ``<colname> IS JSON`` check constraint to indicate to the driver that the
    column stores JSON data.  This constraint is **not** automatically
    generated by :class:`_oracle.JSON` and is not required by SQLAlchemy's
    implementation in order to read JSON data from the column. If desired, it
    can be added explicitly using :class:`_schema.CheckConstraint`.

    .. seealso::

        :class:`_types.JSON` - main documentation for the generic
        cross-platform JSON datatype.

    """

    use_blob: bool | _NoArg

    def __init__(
        self, none_as_null: bool = False, use_blob: bool | _NoArg = NO_ARG
    ):
        """Construct a :class:`_oracle.JSON` type.

        :param none_as_null=False: if True, persist the value ``None`` as a SQL
         NULL value, not the JSON encoding of ``null``.  See the notes at
         :paramref:`_sqltypes.JSON.none_as_null` for complete background on
         this option.

        :param use_blob: A boolean parameter indicating if the type should be
         rendered in DDL using BLOB instead of JSON.   Normally, JSON or BLOB
         is chosen automatically based on the version of Oracle in use
         (21c or greater for JSON).   If the parameter is left at its default
         value of the ``NO_ARG`` constant, this automatic selection is used.
         However when ``True``, the BLOB datatype will be used unconditionally,
         and if ``False``, JSON will be used unconditionally (including on
         backends older than 21c, which will raise an error by the server.
         This may be used to assert that only JSON-supporting backends
         should be used).

        """

        super().__init__(none_as_null=none_as_null)
        self.use_blob = use_blob


class _FormatTypeMixin:
    def _format_value(self, value: Any) -> str:
        raise NotImplementedError()

    def bind_processor(self, dialect: Dialect) -> _BindProcessorType[Any]:
        super_proc = self.string_bind_processor(dialect)  # type: ignore[attr-defined]  # noqa: E501

        def process(value: Any) -> Any:
            value = self._format_value(value)
            if super_proc:
                value = super_proc(value)
            return value

        return process

    def literal_processor(
        self, dialect: Dialect
    ) -> _LiteralProcessorType[Any]:
        super_proc = self.string_literal_processor(dialect)  # type: ignore[attr-defined]  # noqa: E501

        def process(value: Any) -> str:
            value = self._format_value(value)
            if super_proc:
                value = super_proc(value)
            return value  # type: ignore[no-any-return]

        return process


class JSONIndexType(_FormatTypeMixin, sqltypes.JSON.JSONIndexType):
    def _format_value(self, value: Any) -> str:
        if isinstance(value, int):
            return f"$[{value}]"
        else:
            return f'$."{value}"'


class JSONPathType(_FormatTypeMixin, sqltypes.JSON.JSONPathType):
    def _format_value(self, value: Any) -> str:
        return "$%s" % (
            "".join(
                f"[{elem}]" if isinstance(elem, int) else f'."{elem}"'
                for elem in value
            )
        )
