"""Core concept types used when declaring properties and relationships.

These concept types are used as *field types* when you define a
:class:`~relationalai.semantics.frontend.base.Property` or
:class:`~relationalai.semantics.frontend.base.Relationship` in a semantic model.

Common core types include:

- :data:`String` for text values
- :data:`Number` for decimal numeric values (with specific precision and scale defined via :meth:`Number.size`)
- :data:`Integer` for whole numbers (alias for `Number(38, 0)`)
- :data:`Boolean` for true/false values
- :data:`Date` for calendar dates
- :data:`DateTime` for timestamps

Most end users should import core types from :mod:`relationalai.semantics`:

```python
>>> from relationalai.semantics import String, Integer, Number
```

Examples
--------
Use core types in a property definition:

>>> from relationalai.semantics import Integer, Model, String
>>> m = Model()
>>> Person = m.Concept("Person", identify_by={"id": Integer})
>>> Person.name = m.Property(f"{Person} has {String:name}")

Use a concrete decimal type in a property:

>>> Product = m.Concept("Product")
>>> Product.price = m.Property(f"{Product} costs ${Number.size(12, 2):price}")
"""
from __future__ import annotations

import re
from .base import CoreLibrary as Core, Concept, NumberConcept, Field as dslField, Relationship, Library
from ..metamodel.builtins import builtins as b
from ...util.docutils import include_in_docs

__include_in_docs__ = True


#------------------------------------------------------
# Helpers
#------------------------------------------------------
def make_overloads(types: list[Concept], arity: int) -> list[list[Concept]]:
    """ Helper to create a list of overloads with this arity, for each of these types. """
    return list(map(lambda x: [x] * arity, types))

def register_core_alias(name: str, concept: Concept|Relationship):
    """ Register the relation compiled from this concept in buitins.core with a name that is
    different from its concept name. This is useful for operators whose name is not a valid
    python identifier (e.g. "+", "-", etc) so that we can use b.core.plus instead. Also used
    for type aliases like Integer for Number(38,0).
    """
    b.core.__setattr__(name, Core._compiler.to_relation(concept))

#------------------------------------------------------
# Abstract Types
#------------------------------------------------------

@include_in_docs
class AnyNumber(Concept):
    """Represent the family of decimal `Number(precision, scale)` types.

    This class is used to create and reuse concrete number concepts via
    :meth:`AnyNumber.size`. Most users interact with the module-level instance :data:`Number`
    (and aliases like :data:`Integer`) rather than instantiating :class:`AnyNumber` directly.

    Parameters
    ----------
    super_type : Concept
        The numeric super type this number family extends.
    """

    # cache the concrete number concepts created for reuse
    _concrete_numbers = dict[str, NumberConcept]()

    def __init__(self, super_type: Concept):
        super().__init__("Number", extends=[super_type], identify_by={}, model=Core)
        AnyNumber._register(self)

    @include_in_docs
    def size(self, precision:int, scale:int) -> NumberConcept:
        """Return the concrete ``Number(precision, scale)`` concept.

        This method caches number concepts, so repeated calls with the same
        ``precision`` and ``scale`` return the same object.

        Parameters
        ----------
        precision : int
            Total number of digits.
        scale : int
            Number of digits after the decimal point.

        Returns
        -------
        NumberConcept
            The corresponding number concept (for example ``Number.size(38, 0)``).

        Examples
        --------
        Get a concrete numeric type using :data:`Number`:

        >>> from relationalai.semantics.frontend.core import Number
        >>> Number.size(38, 14)
        Number(38,14)

        Calls are cached and common aliases refer to the same concept:

        >>> from relationalai.semantics.frontend.core import Integer
        >>> Number.size(38, 0) is Integer
        True
        >>> Number.size(12, 2) is Number.size(12, 2)
        True
        """
        key = f"{precision},{scale}"
        if key not in self._concrete_numbers:
            nc = NumberConcept(f"Number({precision},{scale})", precision, scale, model=self._model)
            AnyNumber._register(nc)
            self._concrete_numbers[key] = nc
        return self._concrete_numbers[key]

    @include_in_docs
    def parse(self, type_str: str) -> NumberConcept:
        """Parse numeric type strings into a :class:`~relationalai.semantics.frontend.base.NumberConcept`.

        This accepts type strings of the form ``"Number(precision, scale)"`` (or ``"Decimal(precision, scale)"``)
        and returns the cached concept created by :meth:`AnyNumber.size`.

        Parameters
        ----------
        type_str : str
            A type name like ``Number(38, 14)``.

        Returns
        -------
        NumberConcept
            The corresponding number concept.

        Examples
        --------
        Parse a concrete numeric type name using :data:`Number`:

        >>> from relationalai.semantics.frontend.core import Number
        >>> Number.parse("Number(38, 14)")
        Number(38,14)

        ``Decimal`` is accepted as an alias for ``Number``:

        >>> from relationalai.semantics.frontend.core import Integer
        >>> Number.parse("Decimal(38, 0)") is Integer
        True

        Raises
        ------
        ValueError
            If ``type_str`` is not a valid ``Number(precision, scale)`` or ``Decimal(precision, scale)``.
        """
        pattern = r'^(?:Number|Decimal)\(\s*([0-9]+)\s*,\s*([0-9]+)\s*\)$'
        match = re.search(pattern, type_str)
        if match:
            precision, scale = match.group(1), match.group(2)
            return self.size(int(precision), int(scale))
        raise ValueError(f"Invalid Number type name: {type_str}.")

    @classmethod
    def _register(cls, concept: Concept):
        """ Register the concept in the core library and as a builtin. """
        Core.concepts.append(concept)
        Core.concept_index[concept._name] = concept
        Core._register_builtin(concept)


Any = Core.Type("Any")
"""A concept type that matches any value.

This is the top type in the core type hierarchy and is typically used for
fields in relations that accept values of any concrete type.
"""
AnyEntity = Core.Type("AnyEntity")
"""A concept type that matches any entity.

Use this as a super type when a concept represents entities (as opposed to
primitive/value types like ``String`` or ``Number``).
"""
Numeric = Core.Type("Numeric")
"""A concept type for numeric values.

Use this super type for fields in relations that accept any numeric value, including
:data:`Number` and :data:`Float`.
"""
Number = AnyNumber(Numeric)
"""A concept type for decimal numbers.

``Number`` represents the family of concrete number types (for example ``Number(38, 14)``),
and is an instance of :class:`AnyNumber`. Use :meth:`Number.size` to get a specific precision/scale,
or use common aliases like :data:`DefaultNumber` and :data:`Integer`.

Examples
--------
Get a concrete numeric type by specifying precision and scale:

>>> from relationalai.semantics.frontend.core import Number, DefaultNumber, Integer
>>> Number.size(38, 14) is DefaultNumber
True
>>> Number.size(38, 0) is Integer
True

Calls are cached, so repeated requests return the same concept:

>>> Number.size(12, 2) is Number.size(12, 2)
True
"""
ScaledNumber = Core.Type("ScaledNumber", super_types=[Number])
"""A numeric concept type whose precision and scale are inferred from operands.

This is used as the result type for operations like multiplication and division,
where the output number type depends on the input number types.
"""

# A generic type variable that represents any type in a relation, as long as it is consitent
# across all uses within that relation. This allows us to create monotyped overloads, e.g.
# Person < Person and number < number are ok, but Person < Car or number < float are not.
TypeVar = Core.Type("TypeVar")
# Same as TypeVar, but only for entity types (i.e. not value types).
EntityTypeVar = Core.Type("EntityTypeVar")


#------------------------------------------------------
# Meta Types
#------------------------------------------------------
Field = Core.Type("Field")
Relation = Core.Type("Relation")
Type = Core.Type("Type", super_types=[Relation])
Enum = Core.Type("Enum")

#------------------------------------------------------
# Concrete Primitive Types
#------------------------------------------------------
Boolean = Core.Type("Boolean")
"""A concept type for boolean values.

Use this type for fields in properties that represent ``True``/``False`` values.
"""

String = Core.Type("String")
"""A concept type for string values.

Use this type for fields in relationships and properties that represent text.
"""

Date = Core.Type("Date")
"""A concept type for date values.

Use this type for fields in relationships and properties that represent calendar dates.
"""

DateTime = Core.Type("DateTime")
"""A concept type for date-time values.

Use this type for fields in relationships and properties that represent timestamps.
"""

Float = Core.Type("Float", super_types=[Numeric])
"""A concept type for floating-point numeric values.

Use this type for fields in relations that accept floating-point numeric values.
"""

Hash = Core.Type("Hash")
"""A concept type for opaque hash identifiers.

Use this type for fields that store hash IDs produced by the system.
"""

def is_primitive_concept(concept: Concept) -> bool:
    """ Check if the given concept is one of the core primitive concepts. """
    if isinstance(concept, NumberConcept):
        return True
    if (concept is Boolean
            or concept is Number
            or concept is Float
            or concept is String
            or concept is Date
            or concept is DateTime
            or concept is Hash):
        return True
    return any(is_primitive_concept(a) for a in concept._extends)

def extends(concept: Concept, super_concept: Concept) -> bool:
    """ Check if the given concept extends the given super_concept (directly or indirectly). """
    if concept is super_concept:
        return True
    return any(extends(a, super_concept) for a in concept._extends)

#------------------------------------------------------
# Aliases for Types
#------------------------------------------------------
DefaultNumber = Number.size(38, 14) # register default Number type
"""The default decimal number type, `Number(38, 14)`.

Use `DefaultNumber` when you want a general-purpose numeric type without choosing a precision/scale explicitly.
"""
register_core_alias("DefaultNumber", DefaultNumber)
Core.concept_index["DefaultNumber"] = DefaultNumber

Integer = Number.size(38, 0)
"""The integer number type, ``Number(38, 0)``.

Use `Integer` for whole-number fields in relationships and properties.
This is an alias for `Number.size(38, 0)`, which is the largest integer type.
"""
register_core_alias("Integer", Integer)
Core.concept_index["Integer"] = Integer

Int64 = Number.size(19, 0)
"""The 64-bit integer number type, ``Number(19, 0)``.

Use `Int64` for whole-number fields when you want a narrower integer
range than :data:`Integer`.
"""
register_core_alias("Int64", Int64)
Core.concept_index["Int64"] = Int64

Int = Integer
"""Alias for :data:`Integer`.

Use :data:`Int` when you want a shorter name for the core integer type.
"""
Core.concept_index["Int"] = Int

Int128 = Integer
"""Alias for :data:`Integer`.

This name is provided for compatibility with systems that distinguish 64-bit and 128-bit integer types.
"""
Core.concept_index["Int128"] = Int128

Decimal = DefaultNumber
"""Alias for :data:`DefaultNumber`.

Use :data:`Decimal` when you prefer SQL-style naming for the core decimal number family.
"""
Core.concept_index["Decimal"] = Decimal

Bool = Boolean
"""Alias for :data:`Boolean`.

Use :data:`Bool` when you want a shorter name for the core boolean type.
"""
Core.concept_index["Bool"] = Bool

#------------------------------------------------------
# Binary Operators
#------------------------------------------------------
def make_bin_op(name: str, alias: str, overloads=None) -> Relationship:
    """ Helper to create a binary operator relation in the core library. """
    relationship = Core.Relation(name,
        [dslField.input("x", Numeric), dslField.input("y", Numeric), dslField("result", Numeric)],
        overloads=make_overloads([Number, Float], 3) if overloads is None else overloads)
    register_core_alias(alias, relationship)
    return relationship

plus = make_bin_op("+", "plus")
minus = make_bin_op("-", "minus")
mul = make_bin_op("*", "mul", overloads=([Number, Number, ScaledNumber], [Float, Float, Float]))
div = make_bin_op("/", "div", overloads=([Number, Number, ScaledNumber], [Float, Float, Float]))
trunc_div = make_bin_op("//", "trunc_div")
power = make_bin_op("^", "power")
mod = make_bin_op("%", "mod")

#------------------------------------------------------
# Comparison Operators
#------------------------------------------------------
def make_compare_op(name: str, alias: str) -> Relationship:
    """ Helper to create a comparison operator relation in the core library. """
    relationship = Core.Relation(name, [dslField.input("left", TypeVar), dslField.input("right", TypeVar)])
        # overloads=make_overloads([Number, Float, Boolean, String, Date, DateTime], 2))
    register_core_alias(alias, relationship)
    return relationship

eq = make_compare_op("=", "eq")
lt = make_compare_op("<", "lt")
lte = make_compare_op("<=", "lte")
gt = make_compare_op(">", "gt")
gte = make_compare_op(">=", "gte")
neq = make_compare_op("!=", "neq")

#------------------------------------------------------
# Typer-related Relations
#------------------------------------------------------
cast = Core.Relation("cast", [dslField.input("to_type", Type), dslField.input("source", Any), dslField("target", Any)])

#------------------------------------------------------
# Constraint Relations
#------------------------------------------------------

Core.Relation("unique", [dslField.input("fields", Any, is_list=True)])


#------------------------------------------------------
# Downstream annotations
#------------------------------------------------------

Core.Relation("output_value_is_key", [dslField.input("is_key", Integer)])

#------------------------------------------------------
# Core Entities
#------------------------------------------------------

CoreEntities = Library("CoreEntities")

Error = include_in_docs(CoreEntities.Type("Error"))
"""A core :class:`~relationalai.semantics.frontend.base.Concept` for emitting errors.

Use ``Error.new(...)`` inside :meth:`~relationalai.semantics.frontend.base.Model.define` to record an error
when a condition matches. Each call creates an ``Error`` entity with a ``message`` and any extra keyword
fields you provide, for example ``type="InvalidValue"`` which you can later read as ``Error.type``.

If you define multiple errors, select the ones you want by filtering on ``Error.message`` and/or your
context fields (for example ``Error.type == \"InvalidValue\"``).

You can inspect errors explicitly by selecting them, and they also surface as warnings when you run
queries.

Examples
--------
Define an error with context:

>>> from relationalai.semantics import Error, Integer, Model, String
>>> m = Model()
>>> Person = m.Concept("Person")
>>> Person.name = m.Property(f"{Person} has {String:name}")
>>> Person.age = m.Property(f"{Person} has {Integer:age}")
>>> m.define(Person.new(name="Alice", age=10))
>>> m.where(Person.age < 18).define(
...     Error.new(message="Person is too young", person=Person, age=Person.age)
... )

Select only matching errors:

>>> m.where(Error, Error.message == "Person is too young").select(
...     Error.message, Error.person.name, Error.age
... ).to_df()
"""

__all__ = [
    "Any",
    "AnyEntity",
    "Numeric",
    "Number",
    "ScaledNumber",
    "Boolean",
    "String",
    "Date",
    "DateTime",
    "Float",
    "Hash",
    "DefaultNumber",
    "Integer",
    "Int64",
    "Int",
    "Int128",
    "Decimal",
    "Bool",
    "Error",
    "TypeVar",
    "EntityTypeVar",
]
