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

"""
/api/rawfile/: bulk raw-file import endpoint accepting json/yaml/csv request
bodies, optionally routed through a named Pipeline before being loaded via
load_broker. Renamed from /api/import/ (v4.4.0) - client-side CSV chunking
moved to cavctl's "load" subcommand (see /api/load/); this endpoint stays a
thin single-request passthrough, same as it always was.

Method inventory: csv_body, rawfile.
"""

import csv
import json
from io import StringIO

import yaml
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.configuration import get_configuration
from app_home.log import DEBUG, ERROR, log

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


def csv_body(request, pipeline=None, encoding="utf-8"):
    """Parse a text/csv request body into a list of dicts, injecting "classname"
    from the ?schema= param or pipeline.classname; delimiter from ?delimiter=,
    pipeline.csv_delimiter, or the CSV_DELIMITER configuration. Decodes the raw
    body with `encoding` (one of settings.SUPPORTED_ENCODING)."""

    data = []

    csvdata = request.body.decode(encoding)

    csv_delimiter = request.GET.get("delimiter", None)
    if not csv_delimiter:
        if pipeline:
            csv_delimiter = pipeline.csv_delimiter
        else:
            csv_delimiter = get_configuration(appname="home", keyname="CSV_DELIMITER")

    # classname
    classname = request.GET.get("schema", None)
    if not classname:
        if pipeline:
            classname = pipeline.classname

    csvfile = StringIO(csvdata)
    csv_reader = csv.DictReader(csvfile, delimiter=csv_delimiter)

    for entry in csv_reader:
        if classname:
            entry["classname"] = classname
        data.append(entry)

    return data


#  ----------------------------------------------------------------------------
# /api/rawfile/
# ?pipeline=NAME        apply a named Pipeline to each object (optional)
# &schema=NAME           CSV only: classname for every row, overrides any "classname"
#                        column already present; falls back to pipeline.classname if unset (optional)
# &delimiter=,           CSV only: field delimiter (optional)
# &encoding=utf-8         CSV only: body encoding, one of settings.SUPPORTED_ENCODING (optional)
# &action=create|...      override every object's _action (optional)
# &dryrun=true|false      skip the write, shallow (no perm/schema check) (default: false)
# &sync=true|false        touch last_sync on write (parsed by start_api(), see aaa["sync_mode"])
#  ----------------------------------------------------------------------------
@csrf_exempt
def rawfile(request):
    """POST-only: parse the json/yaml/csv body, optionally apply a named Pipeline
    per entry (may discard), then load surviving entries via load_broker."""

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

    # POST only
    # json or yaml

    if request.method != "POST":
        # return helper.send_denied("invalid method")
        return helper.send_error("missing parameters", 405)

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

    # pipeline
    pipeline = None
    pipeline_name = request.GET.get("pipeline", None)
    if pipeline_name:
        pipeline = Pipeline.from_name(pipeline_name)
        if not pipeline:
            log(
                ERROR,
                aaa=aaa_api,
                app="api",
                view="rawfile",
                action="POST",
                status="KO",
                data=f"invalid pipeline {pipeline_name}",
            )
            return helper.send_error("invalid pipeline", 405)

    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)

    encoding = request.GET.get("encoding", "").strip() or "utf-8"
    if encoding not in settings.SUPPORTED_ENCODING:
        return helper.send_error(
            f"unsupported encoding: {encoding} (supported: {', '.join(settings.SUPPORTED_ENCODING)})",
            400,
        )

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

    datalist = []

    # application/yaml / application/x-yaml / text/x-yaml / text/yaml
    if "yaml" in request.content_type:
        try:
            datalist = yaml.safe_load(request.body.decode(encoding))
        except Exception:
            pass

    # application/json / ...
    elif "json" in request.content_type:
        try:
            datalist = json.loads(request.body.decode(encoding))
        except Exception:
            pass

    # text/csv
    elif request.content_type == "text/csv":
        try:
            datalist = csv_body(request, pipeline, encoding)
        except UnicodeDecodeError as exc:
            return helper.send_error(f"encoding error ({encoding}): {exc}", 400)

    else:
        return helper.send_error("invalid content / content-type", 400)

    if not isinstance(datalist, list):
        datalist = [datalist]

    reply = {"count": 0, "count_ok": 0, "count_ko": 0, "errors": [], "dryrun": dryrun}

    for datadict in datalist:
        status = None
        if force_action:
            datadict["_action"] = force_action
        if pipeline:
            status = pipeline.apply(datadict)
        if status == "discard":
            continue
        if dryrun:
            # shallow: never calls load_broker(), so it doesn't check
            # schema/permission - same limitation as /api/load/'s dryrun
            reply["count"] += 1
            reply["count_ok"] += 1
            continue
        result = load_broker(datalist=[datadict], aaa=aaa_api)
        reply["count"] += result.get("count", 0)
        reply["count_ok"] += result.get("count_ok", 0)
        reply["count_ko"] += result.get("count_ko", 0)
        reply["errors"].extend(result.get("errors", []))

    log(DEBUG, aaa=aaa_api, app="api", view="rawfile", action="POST", status="OK", data="")

    return helper.send_response(request, reply, 200)
