# (c) cavaliba.com - data - keyname.py

from celery import shared_task
from django.db import transaction

import app_home.cache as cache
from app_data.models import DataEAV, DataInstance
from app_data.related import get_related
from app_home.log import INFO, TRACE, log

_FORBIDDEN_CLASSNAMES = ("user", "group", "role")

# rename() has no caller identity (CLI/API/task) to attribute referrer updates to
_SYSTEM_AAA = {"username": "system:keyname_rename"}

# never renamable, even with force=True
_PROTECTED_KEYNAMES = {
    "user": {"admin"},
    "role": {"role_admin", "role_default"},
}


def _new_report(classname, oldkeyname, newkeyname, dryrun):
    return {
        "dryrun": dryrun,
        "classname": classname,
        "old_keyname": oldkeyname,
        "new_keyname": newkeyname,
        "target_renamed": False,
        "referrers_scanned": 0,
        "referrers_changed": 0,
        "referrers_by_classname": {},
        "referrer_detail": [],
        "errors": [],
    }


def rename(
    classname=None,
    oldkeyname=None,
    newkeyname=None,
    dryrun=True,
    verbose=False,
    progress=False,
    force=False,
):
    """
    Rename the keyname of a DataInstance and cascade the change to every
    other instance referencing it through a "schema"-type field.
    """

    from app_data.data import Instance

    report = _new_report(classname, oldkeyname, newkeyname, dryrun)

    if not classname:
        report["errors"].append("classname is required")
    if not oldkeyname:
        report["errors"].append("oldkeyname is required")
    if not newkeyname:
        report["errors"].append("newkeyname is required")

    if report["errors"]:
        return report

    if newkeyname == oldkeyname:
        report["errors"].append("newkeyname is identical to oldkeyname - nothing to do")
        return report

    if oldkeyname in _PROTECTED_KEYNAMES.get(classname, ()):
        report["errors"].append(
            f"'{classname}/{oldkeyname}' is a protected built-in identity - rename not allowed"
        )
        return report

    if classname in _FORBIDDEN_CLASSNAMES and not force:
        report["errors"].append(
            f"classname '{classname}' is a derived schema (user/group/role) - "
            "rename not supported, use --force to override"
        )
        return report

    target = Instance.from_keyname(classname=classname, keyname=oldkeyname)
    if not target or not target.is_bound:
        report["errors"].append(f"instance not found: {classname}/{oldkeyname}")
        return report

    builtin_field = target.fields.get("is_builtin")
    if builtin_field is not None and builtin_field.is_field_true():
        report["errors"].append(
            f"'{classname}/{oldkeyname}' is a builtin object (is_builtin) - rename not allowed"
        )
        return report

    if Instance.exists(classname=classname, keyname=newkeyname):
        report["errors"].append(f"newkeyname already exists: {classname}/{newkeyname}")
        return report

    related_map = get_related(classname=classname, keyname=oldkeyname)
    referrers = []
    for ref_classname, entry in related_map.items():
        for eav_row in entry.get("instances", []):
            referrers.append((ref_classname, eav_row.iid))

    if dryrun:
        _scan_referrers(
            Instance,
            classname,
            oldkeyname,
            newkeyname,
            referrers,
            report,
            apply=False,
            verbose=verbose,
        )
        report["referrer_detail"].append(
            {
                "classname": classname,
                "keyname": oldkeyname,
                "fieldname": "(self)",
                "old_values": [oldkeyname],
                "new_values": [newkeyname],
            }
        )
        if verbose:
            print(
                f"keyname rename - DRYRUN - would rename {classname}/{oldkeyname} -> {newkeyname}"
            )
        return report

    with transaction.atomic():
        _scan_referrers(
            Instance,
            classname,
            oldkeyname,
            newkeyname,
            referrers,
            report,
            apply=True,
            verbose=verbose,
        )
        _rename_target(Instance, classname, oldkeyname, newkeyname, target)

    report["target_renamed"] = True

    log(
        INFO,
        app="data",
        view="keyname_rename",
        data=(
            f"{classname}/{oldkeyname} -> {newkeyname} - "
            f"referrers scanned: {report['referrers_scanned']}, changed: {report['referrers_changed']}"
        ),
    )

    if verbose:
        print(f"keyname rename - {classname}/{oldkeyname} -> {newkeyname} - done")

    return report


def _scan_referrers(Instance, classname, oldkeyname, newkeyname, referrers, report, apply, verbose):

    for ref_classname, iid in referrers:
        report["referrers_scanned"] += 1

        ref_instance = Instance.from_id(iid, expand=False)
        if not ref_instance or not ref_instance.is_bound:
            continue

        changed_fields = {}
        for fieldname, field in ref_instance.fields.items():
            if field.is_injected or field.dataformat != "schema":
                continue
            if field.get_eav_format() != f"schema:{classname}":
                continue
            if oldkeyname not in field.value:
                continue

            old_values = list(field.value)
            new_values = [newkeyname if v == oldkeyname else v for v in field.value]
            changed_fields[fieldname] = new_values

            report["referrer_detail"].append(
                {
                    "classname": ref_classname,
                    "keyname": ref_instance.keyname,
                    "fieldname": fieldname,
                    "old_values": old_values,
                    "new_values": new_values,
                }
            )

        if changed_fields:
            report["referrers_changed"] += 1
            report["referrers_by_classname"][ref_classname] = (
                report["referrers_by_classname"].get(ref_classname, 0) + 1
            )
            if verbose:
                log(
                    TRACE,
                    app="data",
                    view="keyname_rename",
                    data=f"referrer: {ref_classname}/{ref_instance.keyname}",
                )
            if apply:
                # merge_import() routes through each field's own merge logic
                # (e.g. composite/list handling) rather than assigning .value directly
                ref_instance.merge_import(changed_fields, aaa=_SYSTEM_AAA)
                ref_instance.save(aaa=_SYSTEM_AAA, action="update")


def _rename_target(Instance, classname, oldkeyname, newkeyname, target):

    target_id = target.id

    DataInstance.objects.filter(pk=target_id).update(keyname=newkeyname)
    DataEAV.objects.filter(classname=classname, keyname=oldkeyname, iid=target_id).delete()

    cache.cache2_instance.delete(f"{classname}::{oldkeyname}")
    cache.cache2_instance.delete(f"{target_id}")
    cache.cache2_datapoint.delete(f"{classname}::{oldkeyname}")
    if classname == "_enumerate":
        cache.cache2_enumerate.delete(oldkeyname)

    renamed = Instance.from_id(target_id, expand=False)
    if renamed and renamed.is_bound:
        # keyname already changed via raw SQL above, so there's no in-memory diff
        # to detect: force the resave purely to rebuild the EAV cache row
        renamed.save(aaa=_SYSTEM_AAA, action="update", force=True)


@shared_task(ignore_result=True)
def task_keyname_rename(
    classname=None,
    oldkeyname=None,
    newkeyname=None,
    dryrun=True,
    verbose=False,
    progress=False,
    force=False,
):
    """async to celery rename"""
    rename(
        classname=classname,
        oldkeyname=oldkeyname,
        newkeyname=newkeyname,
        dryrun=dryrun,
        verbose=verbose,
        progress=progress,
        force=force,
    )
