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

import csv
import json
import os

import yaml
from django.utils.translation import gettext as _

from app_data.data import Instance
from app_data.permissions import has_schema_write_permission, load_permission

# from app_data.data import load_instance, load_schema
from app_data.pipeline import Pipeline
from app_data.schema import Schema
from app_data.user import User
from app_home.configuration import get_configuration
from app_home.load import load_home
from app_home.log import INFO, WARNING, log


# ---------------------------------------------------------------------
# Load broker for one or more objects provided as a LIST of DICTs
# ---------------------------------------------------------------------
def load_broker(datalist=None, aaa=None, force_action=None):
    """
    Load a batch of object into the database

    IN:
       datalist: [ {}, {}, {}, ... ]
       aaa:
       force_action: None, create, update, delete, init, enable, disable, append, unappend

    OUT:
       reply struct (dict) : count, count_ok, count_ko, errors=[]
    """

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

    if not datalist:
        reply["errors"].append("no data provided")
        return reply

    if type(datalist) is not list:
        reply["errors"].append("invalid data provided (not a list)")
        return reply

    if not aaa:
        reply["errors"].append("missing identity")
        return reply

    if "perms" not in aaa:
        reply["errors"].append("missing aaa permissions")
        return reply

    for datadict in datalist:
        err = None

        reply["count"] += 1

        if type(datadict) is not dict:
            reply["count_ko"] += 1
            reply["errors"].append(f"invalid entry: {datadict}")
            continue

        classname = datadict.get("classname", None)
        if not classname:
            reply["count_ko"] += 1
            reply["errors"].append(f"missing classname in {datadict}")
            continue

        if len(classname) == 0:
            # unknown object class : NEXT : log error
            reply["count_ko"] += 1
            reply["errors"].append(f"invalid classname in {datadict}")
            continue

        if force_action:
            datadict["_action"] = force_action

        # load per schema
        if classname == "_schema":
            err = load_schema(datadict=datadict, aaa=aaa)

        # left as regular DB/ORM
        elif classname == "_permission":
            err = load_permission(datadict=datadict, aaa=aaa)

        elif classname == "_home":
            err = load_home(datadict=datadict, aaa=aaa)

        else:
            # internal: _pipeline, _dataview, _apikey, _enumerate
            # apps: sirene_*, ipam_*, status_monitor, ...
            # user-defined schema
            # v4.0 - user, group, role
            err = load_instance(datadict=datadict, aaa=aaa)

        if err:
            reply["count_ko"] += 1
            reply["errors"].append(err)
        else:
            reply["count_ok"] += 1

    return reply


# --------------------------------------------------------
# LOADER / IMPORT
# Global LOADER : class, schema, instance, static
# --------------------------------------------------------


def load_schema(datadict=None, verbose=True, aaa=None):
    """
    IN:
         datadict['keyname']  : mandatory classname(!)

    OUT:
        None or error string
    """

    if not datadict:
        return "load_schema: no data"

    if not has_schema_write_permission(aaa=aaa):
        log(WARNING, aaa=aaa, app="data", view="schema", action="load", status="DENY", data=_("Not allowed"))  # fmt: skip
        return "load_schema: not allowed"

    #  for a _schema, classname is the provided keyname (classname = _schema)
    classname = datadict.get("keyname", None)
    if not classname:
        return f"load_schema: missing classname {datadict}"

    action = datadict.get("_action", "create")

    # delete Schema and Fields
    if action == "delete":
        # TODO: check classname is not builtin (_reserved)
        schema_obj = Schema.delete(classname)
        if schema_obj:
            return
        else:
            return f"load_schema: failed to delete {classname}"

    #  enable
    elif action == "enable":
        # TODO: check classname is not builtin (_reserved)
        schema_obj = Schema.enable(classname)
        if schema_obj:
            return
        else:
            return f"load_schema: failed to enable {classname}"

    # disable
    elif action == "disable":
        # TODO: check classname is not builtin (_reserved)
        schema_obj = Schema.disable(classname)
        if schema_obj:
            return
        else:
            return f"load_schema: failed to disable {classname}"

    # init
    elif action == "init":
        if Schema.exists(classname):
            return
        schema = Schema()
        schema.update_from_dict(datadict, verbose=verbose)
        schema.save()
        # TODO: check save result
        return

    # create (or update)
    elif action == "create":
        schema = Schema.from_name(classname)
        if not schema:
            schema = Schema()
        schema.update_from_dict(datadict, verbose=verbose)
        schema.save()
        return

    # update (only)
    elif action == "update":
        schema = Schema.from_name(classname)
        if not schema:
            return f"load_schema: not found: {classname} "
        schema.update_from_dict(datadict, verbose=verbose)
        schema.save()
        return

    elif action == "noop":
        return

    else:
        log(WARNING, aaa=aaa, app="data", view="schema", action="unknown", status="FAILED", data=f"Unknown load action {action} for schema {classname}")  # fmt: skip

    return f"load_schema: unknown action {action} for {classname}"


def load_instance(datadict=None, aaa=None):

    if not datadict:
        return "load_instance: no data"

    if not aaa:
        log(WARNING, aaa=aaa, app="data", view="instance", action="load", status="DENY", data=_("Not allowed"))  # fmt: skip
        return "load_instance: not allowed"

    classname = datadict.get("classname", None)
    schema = Schema.from_name(classname)
    if not schema:
        return f"load_instance: schema not found: {classname}"

    action = datadict.get("_action", "create")

    keyname = datadict.get("keyname", None)

    # v4.0 -  login backward compatibility for user (login is keyname)
    if not keyname:
        if classname == "user":
            keyname = datadict.get("login", None)

    # schema/user: keyname is an email => resolve to login
    if classname == "user" and keyname and "@" in keyname:
        user = User.get_by_email(email=keyname)
        if not user:
            return f"load_instance: user not found by email {keyname}"
        keyname = user.keyname
        datadict = {**datadict, "keyname": keyname}

    if action == "delete":
        instance = Instance.from_keyname(classname=classname, keyname=keyname, expand=False)
        if instance:
            if instance.has_delete_permission(aaa=aaa):
                r = instance.delete(aaa=aaa)
                if r:
                    log(INFO, aaa=aaa, app="loader", view="instance", action=action, status="OK", data=f"{classname}:{keyname}")  # fmt: skip
                return
            else:
                log(WARNING, aaa=aaa, app="loader", view="instance", action=action, status="DENY", data=_("Not allowed"))  # fmt: skip
        else:
            log(WARNING, aaa=aaa, app="loader", view="instance", action=action, status="DENY", data=_("Not found"))  # fmt: skip
        return f"load_instance: delete failed for {classname}:{keyname}"

    elif action == "disable":
        instance = Instance.from_keyname(classname=classname, keyname=keyname, expand=False)
        if instance:
            if instance.has_update_permission(aaa=aaa):
                r = instance.disable(aaa=aaa)
                if r:
                    log(INFO, aaa=aaa, app="loader", view="instance", action=action, status="OK", data=f"{classname}:{keyname}")  # fmt: skip
                return
            else:
                log(WARNING, aaa=aaa, app="loader", view="instance", action=action, status="DENY", data=_("Not allowed"))  # fmt: skip
        else:
            log(WARNING, aaa=aaa, app="loader", view="instance", action=action, status="DENY", data=_("Not found"))  # fmt: skip
        return f"load_instance: disable failed for {classname}:{keyname}"

    elif action == "enable":
        instance = Instance.from_keyname(classname=classname, keyname=keyname, expand=False)
        if instance:
            if instance.has_update_permission(aaa=aaa):
                r = instance.enable(aaa=aaa)
                if r:
                    log(INFO, aaa=aaa, app="loader", view="instance", action=action, status="OK", data=f"{classname}:{keyname}")  # fmt: skip
                return
            else:
                log(WARNING, aaa=aaa, app="loader", view="instance", action=action, status="DENY", data=_("Not allowed"))  # fmt: skip
        else:
            log(WARNING, aaa=aaa, app="loader", view="instance", action=action, status="DENY", data=_("Not found"))  # fmt: skip
        return f"load_instance: enable failed for {classname}:{keyname}"

    # init only == new instance if doesn't already exist ; else silent noop
    elif action == "init":
        if Instance.exists(classname=classname, keyname=keyname):
            return
        if schema.has_create_permission(aaa):
            instance = Instance(classname=classname)
            if instance:
                instance.merge_import(datadict, aaa=aaa)
                r = instance.save(aaa=aaa)
                if r:
                    log(INFO, aaa=aaa, app="loader", view="instance", action=action, status="OK", data=f"{classname}:{instance.keyname}")  # fmt: skip
                return
            else:
                log(WARNING, aaa=aaa, app="loader", view="instance", action=action, status="DENY", data=_("Not allowed"))  # fmt: skip
                return f"load_instance: init not allowed for {classname}:{keyname}"
        else:
            log(WARNING, aaa=aaa, app="loader", view="instance", action=action, status="DENY", data=_("Couldn't create"))  # fmt: skip
        return f"load_instance: init failed for {classname}:{keyname}"

    # create and/or update if exists
    elif action == "create":
        instance = Instance.from_keyname(classname=classname, keyname=keyname, expand=False)
        if not instance:
            if schema.has_create_permission(aaa):
                instance = Instance(classname=classname)
        if instance:
            if schema.has_create_permission(aaa):
                instance.merge_import(datadict, aaa=aaa)
                r = instance.save(aaa=aaa)
                if r:
                    log(INFO, aaa=aaa, app="loader", view="instance", action=action, status="OK", data=f"{classname}:{instance.keyname}")  # fmt: skip
                return
            else:
                log(WARNING, aaa=aaa, app="loader", view="instance", action=action, status="DENY", data=_("Not allowed"))  # fmt: skip
                return f"load_instance: create not allowed for {classname}:{keyname}"
        else:
            log(WARNING, aaa=aaa, app="loader", view="instance", action=action, status="DENY", data=_("Couldn't create"))  # fmt: skip
        return f"load_instance: create failed for {classname}:{keyname}"

    # don't create, update only if exists
    elif action == "update":
        instance = Instance.from_keyname(classname=classname, keyname=keyname, expand=False)
        if instance:
            if instance.has_update_permission(aaa=aaa):
                instance.merge_import(datadict, aaa=aaa)
                if instance.save(aaa=aaa):
                    log(INFO, aaa=aaa, app="loader", view="instance", action=action, status="OK", data=f"{classname}:{instance.keyname}")  # fmt: skip
                return
            else:
                log(WARNING, aaa=aaa, app="loader", view="instance", action=action, status="DENY", data=_("Not allowed"))  # fmt: skip
                return f"load_instance: update not allowed for {classname}:{keyname}"
        else:
            log(WARNING, aaa=aaa, app="loader", view="instance", action=action, status="DENY", data=_("Not found"))  # fmt: skip
        return f"load_instance: update failed for {classname}:{keyname}"

    # don't create, append/unappend multivalued fields only if exists
    elif action in ("append", "unappend"):
        instance = Instance.from_keyname(classname=classname, keyname=keyname, expand=False)
        if instance:
            if instance.has_update_permission(aaa=aaa):
                instance.merge_import(datadict, aaa=aaa, mode=action)
                if instance.save(aaa=aaa):
                    log(INFO, aaa=aaa, app="loader", view="instance", action=action, status="OK", data=f"{classname}:{instance.keyname}")  # fmt: skip
                return
            else:
                log(WARNING, aaa=aaa, app="loader", view="instance", action=action, status="DENY", data=_("Not allowed"))  # fmt: skip
                return f"load_instance: {action} not allowed for {classname}:{keyname}"
        else:
            log(WARNING, aaa=aaa, app="loader", view="instance", action=action, status="DENY", data=_("Not found"))  # fmt: skip
        return f"load_instance: {action} failed for {classname}:{keyname}"

    elif action == "noop":
        return

    # unknown action
    else:
        log(WARNING, aaa=aaa, app="loader", view="instance", action=action, status="DENY", data=_("Unknown action"))  # fmt: skip

    return f"load_instance: unknown action {action} for {classname}:{keyname}"


# ---------------------------------------------------------------------
# CSV File to DATA
# ---------------------------------------------------------------------

# # custom processing of CSV file with header line and composit field
# def make_dictlist_from_csv(filedata,  splitfields=[]):

#     lines = filedata.splitlines()
#     header = []
#     rows = []
#     line_count = 0

#     delimiter = get_configuration(appname="home", keyname="CSV_DELIMITER")

#     for line in lines:
#         entry = {}
#         fields = line.split(delimiter)
#         if line_count == 0:
#             header = fields
#             line_count = 1
#         else:
#             line_count += 1
#             for i in range(0,len(header)):
#                 hi = header[i]
#                 if hi in splitfields:
#                     try:
#                         a = fields[i].split("+")
#                         entry[hi] = a
#                     except Exception:
#                         pass
#                 else:
#                     try:
#                         entry[hi] = fields[i]
#                     except Exception:
#                         pass
#             rows.append(entry)

#     return rows


def load_file_csv(
    file=None,
    filename=None,
    pipeline_name=None,
    schema_name=None,
    encoding=None,
    csv_delimiter=None,
    verbose=None,
    first=1,
    last=0,
    errors=None,
):
    """
    pipeline_name = keyname of pipeline object (optional). When given, its classname/
    keyfield/encoding/csv_delimiter override everything below (unchanged behavior).

    schema_name = fallback classname, used when no pipeline is given and no per-row
    "schema" column is present. A per-row "schema" column (renamed to "classname")
    always wins over schema_name when no pipeline is given.

    encoding / csv_delimiter = fallback values used when no pipeline is given
    (defaults: utf-8 / CSV_DELIMITER configuration).

    errors = optional caller-supplied list; rows that can't be resolved (missing
    classname or keyname) are skipped and appended to it as {"row": n, "message": str}
    instead of aborting the whole import.

    Multi-valued cells: a cell is split on "^" into a list only if the target schema
    declares that column as a multi-value field (cardinal_max != 1).

    output: list of dict = [ {} , {}, {} ... ]
    {
        classname: _home|user|group|role|_permisison|_schema|_enumerate|_dataview|_pipeline| ...
            _sirene_category|_sirene_public|_sirene_template
            CLASSNAME
        keyname: my key
        displayname: "a nice display name"
        is_enabled:
        (...)
    }
    """

    pipeline = None
    if pipeline_name:
        pipeline = Pipeline.from_name(pipeline_name)
        if not pipeline:
            print("SKIP - invalid pipeline: ", pipeline_name)
            return

    if pipeline and pipeline.csv_delimiter:
        csv_delimiter = pipeline.csv_delimiter
    if not csv_delimiter:
        csv_delimiter = get_configuration(appname="home", keyname="CSV_DELIMITER")

    if pipeline and pipeline.encoding:
        encoding = pipeline.encoding
    if not encoding:
        encoding = "utf-8"

    opened = None
    if file:
        csv_source = file
    else:
        if not filename or not os.path.isfile(filename):
            print(f"SKIP - not a file ({filename})")
            return
        opened = open(filename, encoding=encoding)
        csv_source = opened

    datalist = []
    schema_cache = {}
    cursor = 0

    try:
        csv_reader = csv.DictReader(csv_source, delimiter=csv_delimiter)

        for entry in csv_reader:
            cursor += 1
            if cursor < first:
                continue
            if last > 0 and cursor > last:
                break

            # classname: pipeline > per-row "schema" column > schema_name param
            row_schema = entry.pop("schema", None)
            if pipeline and pipeline.classname:
                classname = pipeline.classname
            elif row_schema:
                classname = row_schema
            else:
                classname = schema_name

            if not classname:
                print("    !skip entry (no classname/schema)")
                if errors is not None:
                    errors.append({"row": cursor, "message": "missing classname/schema"})
                continue
            entry["classname"] = classname

            # keyname: pipeline.keyfield-mapped column > row's own "keyname" column
            if pipeline and pipeline.keyfield:
                keyname = entry.get(pipeline.keyfield, None)
            else:
                keyname = entry.get("keyname", None)

            if not keyname:
                print("    !skip entry (no keyname/keyfield)")
                if errors is not None:
                    errors.append({"row": cursor, "message": f"missing keyname ({classname})"})
                continue
            entry["keyname"] = keyname

            # multi-value split ("^") for schema-declared multi-value fields only
            if classname not in schema_cache:
                schema_cache[classname] = Schema.from_name(classname)
            schema_obj = schema_cache[classname]
            if schema_obj:
                for fieldname, value in list(entry.items()):
                    if not isinstance(value, str) or "^" not in value:
                        continue
                    fielddict = schema_obj.fields.get(fieldname)
                    if fielddict and fielddict.get("cardinal_max", 1) != 1:
                        entry[fieldname] = [v.strip() for v in value.split("^")]

            datalist.append(entry)
    except UnicodeDecodeError as e:
        print(f"SKIP - encoding error in file ({filename}) : {e}")
        return
    finally:
        if opened:
            opened.close()

    return datalist


# ---------------------------------------------------------------------
# JSON to DATA
# ---------------------------------------------------------------------


def load_file_json(file=None, filename=None, pipeline_name=None, verbose=None):

    data = []

    # TODO: pipeline

    # from file
    if file:
        try:
            data = json.load(file)
        except Exception as e:
            print(f"SKIP - JSON syntax error in file : {e}")
            return

    # from filename
    else:
        if not filename:
            print("SKIP - missing filename")
            return

        if not os.path.isfile(filename):
            print(f"SKIP - not a file ({filename})")
            return

        # if not filename.endswith('.json'):
        #     print(f"SKIP - not a JSON file ({filename})")
        #     return

        with open(filename) as file:
            data = json.load(file)

    if type(data) is not list:
        print(f"SKIP - invalid JSON, not a LIST ({filename})")
        return

    return data


# ---------------------------------------------------------------------
# YAML File to DATA
# ---------------------------------------------------------------------
# from file, default from filename
# Second, try filename


def load_file_yaml(file=None, filename=None, pipeline_name=None, verbose=None):

    data = []

    # TODO : pipeline

    # from file
    if file:
        try:
            data = yaml.load(file, Loader=yaml.SafeLoader)
        except Exception as e:
            print("SKIP - YAML syntax error in file : ", e)
            return

    # from filename
    else:
        if not filename:
            print("SKIP - missing filename")
            return

        if not os.path.isfile(filename):
            print(f"SKIP - not a file ({filename})")
            return

        # if not (filename.endswith('.yml') or filename.endswith('.yaml')):
        #     print(f"SKIP - not a YAML file ({filename})")
        #     return

        with open(filename) as f:
            try:
                data = yaml.load(f, Loader=yaml.SafeLoader)
            except Exception as e:
                print(f"SKIP - YAML syntax error in {filename} : ", e)

    if type(data) is not list:
        print(f"SKIP - invalid YAML, not a LIST ({filename})")
        return

    return data
