# (c) cavaliba.com  - data/api - load.py

"""
/api/load/: synchronous small-batch write endpoint. Each JSON object in the
request body is optionally run through a named Pipeline, then written one at a
time via load_broker() - the same engine used by /api/rawfile/, "cavaliba load",
and the UI editor. Unlike /api/rawfile/, there is no file/CSV/YAML parsing (JSON
body only, no delimiter/encoding options). Gated by p_api_load, same as
p_api_rawfile gates /api/rawfile/; the regular per-object create/update
permission is still enforced on top of that, inside load_broker().

Method inventory: load.
"""

import json
import time

from django.conf import settings
from django.views.decorators.csrf import csrf_exempt

import app_data.api.helper as helper
from app_data.api.aaa import start_api
from app_data.loader import load_broker
from app_data.pipeline import Pipeline
from app_home.log import DEBUG, log

_VALID_ACTIONS = ("create", "init", "update", "delete", "enable", "disable")


def _label(datadict):
    """Best-effort (schema, keyname) label for a batch entry, for error reporting."""
    if not isinstance(datadict, dict):
        return None, None
    schema = datadict.get("classname")
    keyname = datadict.get("keyname") or datadict.get("login")
    return schema, keyname


def _process(datadict, pipeline, force_action, dryrun, aaa_api):
    """Apply the optional pipeline then load_broker to a single entry. Returns
    ("ok"|"ko"|"discarded", error_or_None).

    dryrun=True is shallow: it never calls load_broker(), so it never checks
    schema existence or create/update permission - it only reports how many
    objects would be attempted vs. discarded by the pipeline. This matches how
    dryrun already behaves in task_pipeline() and "cavaliba load". A deeper
    dryrun (real permission/schema/field validation, no write) would need
    load_broker()/load_instance() to grow their own dryrun parameter - not
    done here to avoid duplicating that branching logic at the view level.
    """

    if not isinstance(datadict, dict):
        return "ko", f"invalid entry (not an object): {datadict!r}"

    entry = dict(datadict)
    if force_action:
        entry["_action"] = force_action

    if pipeline:
        status = pipeline.apply(entry)
        if status == "discard":
            return "discarded", None

    if dryrun:
        return "ok", None

    reply = load_broker(datalist=[entry], aaa=aaa_api)
    errors = reply.get("errors", [])
    if errors:
        return "ko", "; ".join(str(e) for e in errors)
    return "ok", None


#  ----------------------------------------------------------------------------
# POST /api/load/
# ?pipeline=NAME        apply a named Pipeline to each object (optional)
# &action=create|init|update|delete|enable|disable   override each object's _action (optional)
# &dryrun=true|false    skip the write, report pipeline discard/keep only (default: false)
# &verbose=true|false   include a result entry for every object, not just failures (default: false)
# &sync=true|false      touch last_sync on write (parsed by start_api(), see aaa["sync_mode"])
#  ----------------------------------------------------------------------------
@csrf_exempt
def load(request):
    """POST-only: parse a JSON object/list body, optionally apply a named Pipeline
    per entry (may discard), then write surviving entries one at a time via
    load_broker(). dryrun=true skips the write entirely (shallow: no permission/
    schema validation, see _process()). Aborts early past
    CAVALIBA_LOAD_BATCH_MAX_ERROR failures."""

    aaa_api = start_api(request, permission="p_api_load")
    if not aaa_api["is_allowed"]:
        return helper.send_denied()

    if request.method != "POST":
        return helper.send_error("method not allowed", 405)

    if aaa_api["is_readonly"]:
        return helper.send_denied("API key is read-only")

    try:
        body = json.loads(request.body.decode("utf-8"))
    except Exception as exc:
        return helper.send_error(f"invalid JSON body: {exc}", 400)

    if isinstance(body, dict):
        datalist = [body]
    elif isinstance(body, list):
        datalist = body
    else:
        return helper.send_error("invalid JSON body: expected an object or a list of objects", 400)

    pipeline = None
    pipeline_name = request.GET.get("pipeline", "").strip()
    if pipeline_name:
        pipeline = Pipeline.from_name(pipeline_name)
        if not pipeline:
            return helper.send_error(f"invalid pipeline: {pipeline_name}", 400)

    force_action = request.GET.get("action", "").strip() or None
    if force_action and force_action not in _VALID_ACTIONS:
        return helper.send_error(f"invalid action: {force_action}", 400)

    dryrun = request.GET.get("dryrun", "false").strip().lower() in ("true", "1", "yes")
    verbose = request.GET.get("verbose", "false").strip().lower() in ("true", "1", "yes")

    max_error = settings.CAVALIBA_LOAD_BATCH_MAX_ERROR

    start_time = time.monotonic()

    count_ok = 0
    count_ko = 0
    count_discarded = 0
    aborted = False
    results = []

    for datadict in datalist:
        if count_ko > max_error:
            aborted = True
            break

        schema, keyname = _label(datadict)

        try:
            status, error = _process(datadict, pipeline, force_action, dryrun, aaa_api)
        except Exception as exc:
            status, error = "ko", f"unexpected error: {exc}"

        if status == "ok":
            count_ok += 1
            if verbose:
                results.append({"schema": schema, "keyname": keyname, "status": "ok"})
        elif status == "discarded":
            count_discarded += 1
            if verbose:
                results.append({"schema": schema, "keyname": keyname, "status": "discarded"})
        else:
            count_ko += 1
            results.append({"schema": schema, "keyname": keyname, "status": "ko", "error": error})

    duration = time.monotonic() - start_time

    reply = {
        "dryrun": dryrun,
        "aborted": aborted,
        "count": len(datalist),
        "count_ok": count_ok,
        "count_ko": count_ko,
        "count_discarded": count_discarded,
        "duration": round(duration, 3),
        "results": results,
    }

    log(
        DEBUG,
        aaa=aaa_api,
        app="api",
        view="load",
        action="POST",
        status="OK",
        data=f"batch_size={len(datalist)} ok={count_ok} ko={count_ko} duration={duration:.3f}s",
    )

    return helper.send_response(request, reply, 200)
