# cavaliba - sirene - field.py

"""
Field: base class for one schema field's in-memory value(s) on an Instance.
Subclassed per dataformat in the sibling field_<type>.py modules (see
fieldtypes/__init__.py for the full list).

Method inventory:

Module function:
    split_composite

Construction / debug:
    __init__, print

Cardinality / value access:
    is_multi, is_field_true, get_attribute, get_value, get_first_value,
    get_cardinal3

Export / serialization:
    get_eav_list, get_eav_format, get_snapshot_value, get_datapoint_ui_detail,
    get_datapoint_ui_edit, get_datapoint_for_export, get_csv_cell

Mutation (form/import) - in-memory only:
    merge_request, merge_import

Validation:
    is_valid
"""

from django.conf import settings

from app_data.models import FIELD_FORMAT_CHOICE
from app_home.log import ERROR, log

# --------------------------------------------------------
#  INSTANCE FIELDS
# --------------------------------------------------------


def split_composite(data):
    """Flatten a str/list/dict struct into a flat list [a, b, c, ...]; comma-separated
    strings are split, nested lists are recursed into, dicts are dropped."""

    reply = []

    if type(data) is str:
        result = [x.strip() for x in data.split(",")]
        reply += result

    elif type(data) is list:
        for g in data:
            reply += split_composite(g)

    elif type(data) is dict:
        pass

    else:
        result = [data]
        reply += result

    return reply


class Field:
    # self.fieldname
    # self.fieldschema = {...}
    # self.value = []
    # self.errors

    def __init__(self, fieldname=None, fieldschema=None, alljson=None):
        """Read display/format/cardinality options from fieldschema, then set self.value
        from alljson[fieldname] if present, else from default_value."""

        # to overload in subclass:
        # def __init__(self, fieldname, fieldschema, alljson):
        #     super().__init__(fieldname, fieldschema, alljson)

        #  removed 3.15.7
        # self.fieldschema = fieldschema

        self.fieldname = fieldname

        self.displayname = fieldschema.get("displayname", fieldname)
        self.dataformat = fieldschema.get("dataformat", "string")
        self.dataformat_ext = fieldschema.get("dataformat_ext", "")
        self.cardinal_min = fieldschema.get("cardinal_min", 0)
        self.cardinal_max = fieldschema.get("cardinal_max", 1)
        self.is_enabled = fieldschema.get("is_enabled", True) in settings.TRUE_LIST
        self.value = []
        self.default_value = fieldschema.get("default_value", None)
        self.order = fieldschema.get("order", 100)
        self.page = fieldschema.get("page", "Data")
        self.description = fieldschema.get("description", "")

        # external, schema, enumerate, external_schema, external_enumerate
        # self.is_injected = False
        # self.injected_type = ""
        self.is_injected = fieldschema.get("is_injected", False)
        self.injected_type = fieldschema.get("injected_type", "")
        self.errors = []

        # default from schema is a "ui string" ; need to convert to a list
        # self.default_value = [self.default_value]

        # json value is already a list : either from DB
        # or from merge_new_post_request POST.getlist()

        # set value from json
        if alljson:
            self.value = alljson.get(fieldname, None)
            if type(self.value) is not list:
                self.value = []
        #  else set value to default_value
        else:
            if self.default_value:
                if type(self.default_value) is list:
                    self.value = self.default_value
                else:
                    self.value = [self.default_value]

        # if exists, with mandatory value / none set / default available
        if self.cardinal_min > 0:
            if len(self.value) == 0:
                if self.default_value:
                    if type(self.default_value) is list:
                        self.value = self.default_value
                    else:
                        self.value = [self.default_value]

    def print(self):
        """Print "fieldname = value" (debug helper)."""
        try:
            # value_str = '|'.join(self.value)
            print(f"  Field: {self.fieldname} = {self.value}")
        except Exception:
            pass

    def is_multi(self):
        """Return True if the field accepts more than one value (cardinal_max != 1)."""
        return self.cardinal_max != 1

    def is_field_true(self):
        """Return False; overloaded in FieldBoolean to test a single boolean value."""
        return False

    def get_eav_list(self):
        """
        OUT: list of string from self.value for DB DataEAV value field
        """
        try:
            return [str(i) for i in self.value]
        except Exception:
            return ["?"]

    def get_eav_format(self):
        """Return the dataformat string stored in the DataEAV row; overloaded when a
        variant needs a qualified format (e.g. "ipv4:subnet", "schema:classname")."""
        return self.dataformat

    # TODO : split raw/processed field
    def get_attribute(self):
        """Return the raw JSON value list, e.g. ["a", "b", ...]; overloaded in
        FieldSchema/FieldPermission to resolve to related objects instead."""
        return self.value

    def get_value(self):
        """Return self.value as-is (list of raw values)."""
        return self.value

    def get_snapshot_value(self):
        """Return value for revision diff snapshots. Drops None/empty-string entries so
        unset-vs-blank doesn't produce spurious revision diffs. Override in subclasses
        (e.g. boolean) to normalize representations differently."""
        return [v for v in self.value if v not in (None, "")]

    # return first value / process in overload
    def get_first_value(self):
        """Return self.value[0], or "" if empty; overloaded where a typed cast is needed
        (e.g. FieldBoolean, FieldSchema)."""
        try:
            return self.value[0]
        except Exception:
            return ""

    def get_cardinal3(self):
        """Return (current count, cardinal_min, cardinal_max) for this field's value."""

        cardinal = len(self.value)
        cardinal_min = self.cardinal_min
        cardinal_max = self.cardinal_max

        return cardinal, cardinal_min, cardinal_max

    def get_datapoint_ui_detail(self):
        """Build the datapoint dict for the read-only detail/list UI template
        (fieldname, displayname, formatted value string, ...)."""

        datapoint = {}
        datapoint["fieldname"] = self.fieldname
        datapoint["displayname"] = self.displayname
        datapoint["description"] = self.description
        datapoint["dataformat"] = self.dataformat
        datapoint["dataformat_ext"] = self.dataformat_ext
        datapoint["is_multi"] = self.is_multi()
        datapoint["cardinal_min"] = self.cardinal_min
        datapoint["cardinal_max"] = self.cardinal_max
        datapoint["bigset"] = False
        datapoint["value"] = ""

        if self.value:
            if len(self.value) == 1:
                datapoint["value"] = self.value[0]
            elif len(self.value) > 1:
                # TODO : join self.value.display only
                try:
                    # datapoint["value"] = ', '.join(str(self.value))
                    datapoint["value"] = ", ".join([str(z) for z in self.value])
                    # datapoint["value"] = 'XXX'
                except Exception:
                    pass

        return datapoint

    def get_datapoint_ui_edit(self):
        """Build the datapoint dict for the new/edit form UI template (fieldname,
        displayname, dataformat, raw value list); overloaded per dataformat."""

        # default / parent behavior
        # surcharged in subclass

        df = self.dataformat
        if df not in dict(FIELD_FORMAT_CHOICE):
            log(
                ERROR,
                app="data",
                view="field",
                action="get_datapoint_ui_edit",
                status="KO",
                data=f"unknown dataformat '{df}' for field '{self.fieldname}' - fallback to unknown",
            )
            df = "unknown"

        datapoint = {}
        datapoint["fieldname"] = self.fieldname
        datapoint["displayname"] = self.displayname
        datapoint["description"] = self.description
        datapoint["dataformat"] = df
        datapoint["dataformat_ext"] = self.dataformat_ext
        datapoint["is_multi"] = self.is_multi()
        datapoint["value"] = self.value

        return datapoint

    def get_datapoint_for_export(self):
        """Return the exportable value: single value if not multi (or "" if empty),
        else the full value list."""

        if not self.is_multi():
            try:
                return self.value[0]
            except Exception:
                # empty
                return ""
        return self.value

    def get_csv_cell(self):
        """Return the CSV cell content: single value if not multi, else values joined by "^"."""
        if not self.is_multi():
            try:
                return self.value[0]
            except Exception:
                # empty
                return ""
        else:
            try:
                return "^".join(self.value)
            except Exception:
                return ""

        return self.value

    # MERGE REQUEST
    #  -------------
    def merge_request(self, request):
        """Set self.value from request.POST.getlist(fieldname); overloaded per dataformat
        for type-specific parsing (int, float, boolean, file, password, ...)."""

        # default
        data = request.POST.getlist(self.fieldname, default=[])

        self.value = []

        # default behavior
        if not data:
            return

        if len(data) == 0:
            return

        if type(data) is not list:
            try:
                if len(data) > 0:
                    self.value = [data]
            except Exception:
                return
        else:
            for i in data:
                if len(i) > 0:
                    self.value.append(i)

    #  IMPORT
    # ------
    def merge_import(self, data):
        """Set self.value from an import/API payload via split_composite(); overloaded
        per dataformat where composite-splitting is wrong (date, string, text, ...)."""
        # default behavior
        self.value = split_composite(data)

    def is_valid(self):
        """Check cardinality (cardinal_min/cardinal_max) against len(self.value);
        overloaded per dataformat to add type/format/range checks."""

        # check cardinality
        cardinal = len(self.value)
        if cardinal < self.cardinal_min:
            return False
        if self.cardinal_max > 0 and cardinal > self.cardinal_max:
            return False

        return True
