# mypy: disable-error-code = no-untyped-call
import sys
from typing import Any

from django.apps import apps
from django.db import models
from django.utils.module_loading import import_string

import pghistory
from pghistory import (
    ContextForeignKey,
    ContextJSONField,
    ContextUUIDField,
    DeleteEvent,
    InsertEvent,
    ObjForeignKey,
    UpdateEvent,
    config,
    constants,
    utils,
)
from pghistory.core import (
    _generate_event_model_name,
    _generate_history_field,
    _get_append_only,
    _get_context_field,
    _get_context_id_field,
    _get_obj_field,
    _validate_event_model_path,
    track,
)
import pgtrigger

from console_base.models import BaseIntegerPKModel, BaseUUIDPKModel
from .models import ConcordEvent


def cctrack(*args: Any, **kwargs) -> ConcordEvent:  # type: ignore[no-untyped-def]
    """
    Wrapper around builtin `track` decorator to always
    exclude the `created` and `modified` fields.

    If `modified` is the only changed value, (for sync purposes)
    we can skip creating a version of it.
    """
    kwargs['exclude'] = _exclude_date_fields(exclude=list(kwargs.get('exclude') or []))

    if not kwargs.get('base_model'):
        kwargs['base_model'] = ConcordEvent

    context_field = kwargs.get('context_field')
    if not context_field or context_field == pghistory.constants.UNSET:
        kwargs['context_field'] = pghistory.ContextJSONField()

    kwargs['meta'] = _set_indexes(kwargs.pop('meta', {}))
    return track(*args, **kwargs)


def cc_create_event_model(  # type: ignore[no-untyped-def]
    tracked_model: type[BaseIntegerPKModel] | type[BaseUUIDPKModel],
    *trackers,
    fields: list[str] | None = None,
    exclude: list[str] | None = None,
    obj_field: ObjForeignKey | constants.Unset = constants.UNSET,
    context_field: ContextForeignKey | ContextJSONField | constants.Unset = constants.UNSET,
    context_id_field: ContextUUIDField | constants.Unset = constants.UNSET,
    append_only: bool | constants.Unset = constants.UNSET,
    model_name: str | None = None,
    app_label: str | None = None,
    base_model: type[models.Model] | None = None,
    attrs: dict[str, Any] | None = None,
    meta: dict[str, Any] | None = None,
    abstract: bool = True,
) -> type[ConcordEvent]:
    """
    Override the builtin `create_event_model` function to always
    pass in default list of row events. Using `PGHISTORY_DEFAULT_TRACKERS`
    doesn't work because instantiated classes generate triggers polluted
    accross models. :scream:

    Create an event model.

    Instead of using [pghistory.track][], which dynamically genferates an event
    model, one can instead construct a event model themselves, which
    will also set up event tracking for the tracked model.

    Args:
        tracked_model: The model that is being tracked.
        *trackers: The event trackers. When using any tracker that inherits
            [pghistory.RowEvent][], such as [pghistory.InsertEvent][], a
            Postgres trigger will be installed that automatically stores the event
            into the generated event model. Trackers that do not inherit
            [pghistory.RowEvent][] must be manually created. If no events are
            supplied, defaults to `pghistory.InsertEvent` and `pghistory.UpdateEvent`.
        fields: The list of fields to snapshot when the event takes place. When
            no fields are provided, the entire model is snapshot when the event
            happens. Note that snapshotting of the OLD or NEW row is configured
            by the `snapshot` attribute of the `DatabaseTracker` object. Manual
            events must specify these fields during manual creation.
        exclude: Instead of providing a list of fields to snapshot, a user can
            instead provide a list of fields to not snapshot.
        obj_field: The foreign key field configuration that references the tracked object.
            Defaults to an unconstrained non-nullable foreign key. Use `None` to create a
            event model with no reference to the tracked object.
        context_field: The context field configuration. Defaults to a nullable
            unconstrained foreign key. Use `None` to avoid attaching historical context altogether.
        context_id_field: The context ID field configuration when using a ContextJSONField
            for the context_field. When using a denormalized context field, the ID
            field is used to track the UUID of the context. Use `None` to avoid using this
            field for denormalized context.
        append_only: True if the event model is protected against updates and deletes.
        model_name: Use a custom model name when the event model is generated. Otherwise
            a default name based on the tracked model and fields will be created.
        app_label: The app_label for the generated event model. Defaults to the app_label
            of the tracked model. Note, when tracking a Django model (User) or a model
            of a third-party app, one must manually specify the app_label of an internal
            app to use so that migrations work properly.
        base_model: The base model for the event model. Must inherit pghistory.models.Event.
        attrs: Additional attributes to add to the event model
        meta: Additional attributes to add to the Meta class of the event model.
        abstract: `True` if the generated model should be an abstract model.

    Returns:
        The event model class.

    Example:
        Create a custom event model:

            class MyEventModel(create_event_model(
                TrackedModel,
                pghistory.InsertEvent(),
            )):
                # Add custom indices or change default field declarations...
    """  # noqa

    # ------------------------------------------------------------------
    # Override `create_event_model` function to provide default trackers
    # that must be instantiated each time the function is called.
    # Passing in instantiated event classes via settings doesn't
    # generate correct triggers.
    if not trackers:
        trackers = [InsertEvent(), UpdateEvent(), DeleteEvent()]  # type: ignore[assignment]

    # Add a few more default variables
    exclude = _exclude_date_fields(exclude=exclude or [])
    context_field = context_field or pghistory.ContextJSONField()
    base_model = base_model or ConcordEvent
    meta = _set_indexes(meta or {})
    # ------------------------------------------------------------------

    # ------------------------------------------------------------------
    # All code below this line should be default function.
    # ------------------------------------------------------------------

    event_model = import_string("pghistory.models.Event")
    base_model = base_model or config.base_model()
    assert issubclass(base_model, event_model)

    obj_field = _get_obj_field(
        obj_field=obj_field,
        tracked_model=tracked_model,
        base_model=base_model,
        fields=fields,
    )
    context_field = _get_context_field(context_field)
    context_id_field = _get_context_id_field(context_id_field)
    append_only = _get_append_only(append_only)

    model_name = model_name or _generate_event_model_name(base_model, tracked_model, fields)
    app_label = app_label or tracked_model._meta.app_label
    _validate_event_model_path(app_label=app_label, model_name=model_name, abstract=abstract)
    app = apps.app_configs[app_label]
    app_module_name = getattr(app.module, '__name__')
    models_module = f"{app_module_name}.models"

    attrs = attrs or {}
    attrs.update({"pgh_trackers": trackers})
    meta = meta or {}
    exclude = exclude or []
    fields = fields or [f.name for f in tracked_model._meta.fields if f.name not in exclude]

    if append_only:
        meta["triggers"] = [
            *meta.get("triggers", []),
            pgtrigger.Protect(name="append_only", operation=pgtrigger.Update | pgtrigger.Delete),
        ]

    class_attrs = {
        "__module__": models_module,
        "Meta": type("Meta", (), {"abstract": abstract, "app_label": app_label, **meta}),
        "pgh_tracked_model": tracked_model,
        **{field: _generate_history_field(tracked_model, field) for field in fields},
        **attrs,
    }

    if isinstance(context_field, utils.JSONField) and context_id_field:
        class_attrs["pgh_context_id"] = context_id_field

    if context_field:
        class_attrs["pgh_context"] = context_field

    if obj_field:
        class_attrs["pgh_obj"] = obj_field

    event_model = type(model_name, (base_model,), class_attrs)
    if not abstract:
        setattr(sys.modules[models_module], model_name, event_model)

    return event_model


def _exclude_date_fields(exclude: list[str]) -> list[str]:
    date_fields = ['created', 'modified']
    if not exclude:
        return date_fields

    exclude.extend(date_fields)
    return list(set(exclude))


def _set_indexes(meta: dict) -> dict:
    indexes: list[models.Index] = meta.get('indexes') or []
    indexes.extend([
        models.Index(fields=['cid']),
        models.Index(fields=['pgh_obj']),
        models.Index(fields=['pgh_created_at']),
    ])
    meta['indexes'] = indexes

    return meta


__all__ = (
    'cctrack',
    'cc_create_event_model',
)
