import datetime
import json
import logging
import time
import urllib.parse
import uuid
from abc import abstractmethod
from textwrap import wrap
from typing import List

import pymarc
from folio_uuid.folio_uuid import FOLIONamespaces
from folio_uuid.folio_uuid import FolioUUID
from folioclient import FolioClient
from pymarc import Field
from pymarc import Leader
from pymarc import Record

from folio_migration_tools.custom_exceptions import TransformationFieldMappingError
from folio_migration_tools.custom_exceptions import TransformationProcessError
from folio_migration_tools.custom_exceptions import TransformationRecordFailedError
from folio_migration_tools.helper import Helper
from folio_migration_tools.library_configuration import FileDefinition
from folio_migration_tools.library_configuration import LibraryConfiguration
from folio_migration_tools.mapper_base import MapperBase
from folio_migration_tools.marc_rules_transformation.hrid_handler import HRIDHandler
from folio_migration_tools.report_blurbs import Blurbs


class RulesMapperBase(MapperBase):
    def __init__(
        self,
        folio_client: FolioClient,
        library_configuration: LibraryConfiguration,
        task_configuration,
        schema: dict,
        conditions=None,
        parent_id_map: dict = None,
    ):
        super().__init__(library_configuration, folio_client, parent_id_map)
        self.parsed_records = 0
        self.id_map: dict = {}
        self.start = time.time()
        self.last_batch_time = time.time()
        self.folio_client: FolioClient = folio_client
        self.schema: dict = schema
        self.task_configuration = task_configuration
        self.conditions = conditions
        self.item_json_schema = ""
        self.mappings: dict = {}
        self.schema_properties = None
        if hasattr(self.task_configuration, "hrid_handling"):
            self.hrid_handler = HRIDHandler(
                folio_client,
                self.task_configuration.hrid_handling,
                self.migration_report,
                self.task_configuration.deactivate035_from001,
            )
        logging.info("Current user id is %s", self.folio_client.current_user)

    def print_progress(self):
        self.parsed_records += 1
        num_recs = 5000
        if self.parsed_records % num_recs == 0:
            elapsed = self.parsed_records / (time.time() - self.start)
            elapsed_formatted = "{0:.4g}".format(elapsed)
            elapsed_last = num_recs / (time.time() - self.last_batch_time)
            elapsed_formatted_last = "{0:.4g}".format(elapsed_last)
            logging.info(
                f"{elapsed_formatted_last} (avg. {elapsed_formatted}) "
                f"records/sec.\t\t{self.parsed_records:,} records processed"
            )
            self.last_batch_time = time.time()

    @abstractmethod
    def get_legacy_ids(self, marc_record: Record, idx: int):
        raise NotImplementedError()

    @staticmethod
    def dedupe_rec(rec, props_to_not_dedupe=None):
        if props_to_not_dedupe is None:
            props_to_not_dedupe = []
        # remove duplicates
        for key, value in rec.items():
            if key not in props_to_not_dedupe and isinstance(value, list):
                res = []
                for v in value:
                    if v not in res:
                        res.append(v)
                rec[key] = list(res)

    def map_field_according_to_mapping(
        self, marc_field: pymarc.Field, mappings, folio_record, legacy_ids
    ):
        for mapping in mappings:
            if "entity" not in mapping:
                self.handle_normal_mapping(mapping, marc_field, folio_record, legacy_ids)
            else:
                self.handle_entity_mapping(
                    marc_field,
                    mapping,
                    folio_record,
                    legacy_ids,
                )

    def handle_normal_mapping(self, mapping, marc_field: pymarc.Field, folio_record, legacy_ids):
        target = mapping["target"]
        if mapping.get("ignoreSubsequentSubfields", False):
            marc_field = self.remove_repeated_subfields(marc_field)
        if has_conditions(mapping):
            values = self.apply_rules(marc_field, mapping, legacy_ids)
            if marc_field.tag == "655":
                values[0] = f"Genre: {values[0]}"
            self.add_value_to_target(folio_record, target, values)
        elif has_value_to_add(mapping):
            value = mapping["rules"][0]["value"]
            # Stupid construct to avoid bool("false") == True
            if value == "true":
                self.add_value_to_target(folio_record, target, [True])
            elif value == "false":
                self.add_value_to_target(folio_record, target, [False])
            else:
                self.add_value_to_target(folio_record, target, [value])
        else:
            # Adding stuff without rules/Conditions.
            # Might need more complex mapping for arrays etc
            if any(mapping["subfield"]):
                value = " ".join(marc_field.get_subfields(*mapping["subfield"]))
            else:
                value = marc_field.format_field() if marc_field else ""
            self.add_value_to_target(folio_record, target, [value])

    @staticmethod
    def set_005_as_updated_date(marc_record: Record, folio_object: dict, legacy_ids):
        try:
            f005 = marc_record["005"].data[:14]
            parsed_date = datetime.datetime.strptime(f005, "%Y%m%d%H%M%S").isoformat()
            folio_object["metadata"]["updatedDate"] = parsed_date
        except Exception as exception:
            if "005" in marc_record:
                Helper.log_data_issue(
                    legacy_ids,
                    f"Could not parse Last transaction date from 005 {exception}",
                    marc_record["005"].data,
                )

    @abstractmethod
    def parse_record(
        self, marc_record: Record, file_def: FileDefinition, legacy_ids: List[str]
    ) -> dict:
        raise NotImplementedError()

    @staticmethod
    def use_008_for_dates(marc_record: Record, folio_object: dict, legacy_ids):
        try:
            first_six = "".join(marc_record["008"].data[:6])
            date_str = f"19{first_six}" if int(first_six[:2]) > 69 else f"20{first_six}"
            date_str_parsed = datetime.datetime.strptime(date_str, "%Y%m%d")
            if "title" in folio_object:  # only instance has titles
                folio_object["catalogedDate"] = date_str_parsed.strftime("%Y-%m-%d")
        except Exception as exception:
            if "008" in marc_record:
                Helper.log_data_issue(
                    legacy_ids,
                    f"Could not parse cat date from 008 {exception}",
                    marc_record["008"].data,
                )

    def get_value_from_condition(
        self,
        legacy_id,
        mapping,
        marc_field,
    ):
        stripped_conds = mapping["rules"][0]["conditions"][0]["type"].split(",")
        condition_types = list(map(str.strip, stripped_conds))
        parameter = mapping["rules"][0]["conditions"][0].get("parameter", {})
        if mapping.get("applyRulesOnConcatenatedData", ""):
            value = " ".join(marc_field.get_subfields(*mapping["subfield"]))
            return self.apply_rule(legacy_id, value, condition_types, marc_field, parameter)
        elif mapping.get("subfield", []):
            subfields = marc_field.get_subfields(*mapping["subfield"])
            x = [
                self.apply_rule(legacy_id, x, condition_types, marc_field, parameter)
                for x in subfields
            ]
            return " ".join(set(x))
        else:
            value1 = marc_field.format_field() if marc_field else ""
            return self.apply_rule(legacy_id, value1, condition_types, marc_field, parameter)

    def process_marc_field(
        self,
        folio_record: dict,
        marc_field: Field,
        ignored_subsequent_fields,
        legacy_ids,
    ):
        if marc_field.tag == "880":
            mappings = self.perform_proxy_mapping(marc_field)
        else:
            tags_to_ignore = {"880", "001", "008"}
            mappings = (
                self.mappings.get(marc_field.tag, {})
                if marc_field.tag not in tags_to_ignore
                else []
            )
        if mappings:
            try:
                self.map_field_according_to_mapping(marc_field, mappings, folio_record, legacy_ids)
                if any(m.get("ignoreSubsequentFields", False) for m in mappings):
                    ignored_subsequent_fields.add(marc_field.tag)
            except TransformationFieldMappingError as tre:
                tre.log_it()
                logging.error(
                    "map_field_according_to_mapping %s %s %s %s %s",
                    "-".join(legacy_ids),
                    marc_field.tag,
                    marc_field.format_field(),
                    json.dumps(mappings),
                    tre.message,
                )
            except Exception as ee:
                logging.error(
                    "map_field_according_to_mapping %s %s %s",
                    marc_field.tag,
                    marc_field.format_field(),
                    json.dumps(mappings),
                )
                raise ee

    def perform_proxy_mapping(self, marc_field):
        proxy_mapping = next(iter(self.mappings.get("880", [])), [])
        if "6" not in marc_field:
            self.migration_report.add(Blurbs.Field880Mappings, "Records without $6")
            return None
        if not proxy_mapping or not proxy_mapping.get("fieldReplacementBy3Digits", False):
            return None
        if not marc_field["6"][:3] or len(marc_field["6"][:3]) != 3:
            self.migration_report.add(
                Blurbs.Field880Mappings, "Records with unexpected length in $6"
            )
            return None
        first_three = marc_field["6"][:3]

        target_field = next(
            (
                r.get("targetField", "")
                for r in proxy_mapping.get("fieldReplacementRule", [])
                if r["sourceDigits"] == first_three
            ),
            first_three,
        )
        self.migration_report.add(
            Blurbs.Field880Mappings,
            f"Source digits: {marc_field['6']} Target field: {target_field}",
        )
        mappings = self.mappings.get(target_field, {})
        if not mappings:
            self.migration_report.add(
                Blurbs.Field880Mappings,
                f"Mapping not set up for target field: {target_field} ({marc_field['6']})",
            )
        return mappings

    def report_marc_stats(
        self, marc_field: Field, bad_tags, legacy_ids, ignored_subsequent_fields
    ):
        self.migration_report.add(Blurbs.Trivia, "Total number of Tags processed")
        self.report_source_and_links(marc_field)
        self.report_bad_tags(marc_field, bad_tags, legacy_ids)
        mapped = marc_field.tag in self.mappings
        if marc_field.tag in ignored_subsequent_fields:
            mapped = False
        self.report_legacy_mapping(marc_field.tag, True, mapped)

    def report_source_and_links(self, marc_field: Field):
        for subfield_2 in marc_field.get_subfields("2"):
            self.migration_report.add(
                Blurbs.AuthoritySources, f"Source of heading or term: {subfield_2.split(' ')[0]}"
            )
        for subfield_0 in marc_field.get_subfields("0"):
            code = ""
            if "(" in subfield_0 and ")" in subfield_0:
                code = subfield_0[subfield_0.find("(") + 1 : subfield_0.find(")")]
                code = code.split(" ")[0]
            elif url := urllib.parse.urlparse(subfield_0):
                if url.hostname:
                    code = subfield_0[: subfield_0.find(url.path)]
            if code:
                self.migration_report.add(
                    Blurbs.AuthoritySources, f"$0 base uri or source code: {code}"
                )

    def apply_rules(self, marc_field: pymarc.Field, mapping, legacy_ids):
        try:
            values = []
            value = ""
            if has_conditions(mapping):
                value = self.get_value_from_condition(",".join(legacy_ids), mapping, marc_field)
            elif has_value_to_add(mapping):
                value = mapping["rules"][0]["value"]
                if value == "false":
                    return [False]
                elif value == "true":
                    return [True]
                else:
                    return [value]
            elif not mapping.get("rules", []) or not mapping["rules"][0].get("conditions", []):
                value = " ".join(marc_field.get_subfields(*mapping["subfield"]))
            values = wrap(value, 3) if mapping.get("subFieldSplit", "") else [value]
            return values
        except TransformationProcessError as trpe:
            self.handle_transformation_process_error(self.parsed_records, trpe)
        except TransformationFieldMappingError as fme:
            self.migration_report.add(Blurbs.FieldMappingErrors, fme.message)
            fme.data_value = (
                f"{fme.data_value} MARCField: {marc_field} Mapping: {json.dumps(mapping)}"
            )
            fme.log_it()
            return []
        except TransformationRecordFailedError as trfe:
            trfe.data_value = (
                f"{trfe.data_value} MARCField: {marc_field} Mapping: {json.dumps(mapping)}"
            )
            trfe.log_it()
            self.migration_report.add_general_statistics(
                "Records failed due to an error. See data issues log for details"
            )
        except Exception as exception:
            self.handle_generic_exception(self.parsed_records, exception)

    def report_bad_tags(self, marc_field, bad_tags, legacy_ids):
        if (
            (not marc_field.tag.isnumeric())
            and marc_field.tag != "LDR"
            and marc_field.tag not in bad_tags
        ):
            self.migration_report.add(Blurbs.NonNumericTagsInRecord, marc_field.tag)
            message = "Non-numeric tags in records"
            Helper.log_data_issue(legacy_ids, message, marc_field.tag)
            bad_tags.add(marc_field.tag)

    def add_value_to_target(self, rec, target_string, value):
        if not value:
            return
        targets = target_string.split(".")
        if len(targets) == 1:
            self.add_value_to_first_level_target(rec, target_string, value)
        else:
            schema_parent = None
            parent = None
            schema_properties = self.schema["properties"]
            sc_prop = schema_properties
            for target in targets:  # Iterate over names in hierarcy
                if target in sc_prop:  # property is on this level
                    sc_prop = sc_prop[target]  # set current property
                else:  # next level. take the properties from the items
                    sc_prop = schema_parent["items"]["properties"][target]
                if target not in rec and not schema_parent:  # have we added this already?
                    if is_array_of_strings(sc_prop):
                        rec[target] = []
                        # break
                        # prop[target].append({})
                    elif is_array_of_objects(sc_prop):
                        rec[target] = [{}]
                        # break
                    elif (
                        schema_parent
                        and is_array_of_objects(schema_parent)
                        and sc_prop.get("type", "string") == "string"
                    ):
                        s = "This should be unreachable code. Check schema for changes"
                        logging.error(s)
                        logging.error(parent)
                        raise TransformationProcessError("", s)
                        # break
                    else:
                        if schema_parent["type"] == "array":
                            parent.append({})
                        else:
                            raise TransformationProcessError(
                                "",
                                f"Edge! Something in the schemas has changed. "
                                "The mapping of this needs to be investigated "
                                f"{target_string} {schema_properties[target_string]}",
                            )
                elif is_array_of_objects(sc_prop) and len(rec[target][-1]) == len(
                    sc_prop["items"]["properties"]
                ):
                    rec[target].append({})
                elif schema_parent and target in rec[parent][-1]:
                    rec[parent].append({})
                    if len(rec[parent][-1]) > 0:
                        rec[parent][-1][target] = value[0]
                    else:
                        rec[parent][-1] = {target: value[0]}
                elif (
                    schema_parent
                    and is_array_of_objects(schema_parent)
                    and sc_prop.get("type", "string") == "string"
                ):
                    if len(rec[parent][-1]) > 0:
                        rec[parent][-1][target] = value[0]
                    else:
                        rec[parent][-1] = {target: value[0]}
                # if target == targets[-1]:
                # prop[target] = value[0]
                # prop = rec[target]
                schema_parent = sc_prop
                parent = target

    def add_value_to_first_level_target(self, rec, target_string, value):
        sch = self.schema["properties"]

        if not target_string or target_string not in sch:
            raise TransformationFieldMappingError(
                "",
                (
                    f"Target string '{target_string}' not in Schema! "
                    "Check mapping file against the schema. "
                    f"Target type: {sch.get(target_string,{}).get('type','')} Value: {value}"
                ),
                "",
            )

        target_field = sch.get(target_string, {})
        if (
            target_field.get("type", "") == "array"
            and target_field.get("items", {}).get("type", "") == "string"
        ):

            if target_string not in rec:
                rec[target_string] = value
            else:
                rec[target_string].extend(value)

        elif target_field.get("type", "") == "string":
            if value[0]:
                rec[target_string] = value[0]
        else:
            raise TransformationProcessError(
                "",
                (
                    f"Edge! Target string: {target_string} "
                    f"Target type: {sch.get(target_string,{}).get('type','')} Value: {value}"
                ),
            )

    def remove_from_id_map(self, former_ids: List[str]):
        """removes the ID from the map in case parsing failed

        Args:
            former_ids (_type_): _description_
        """
        for former_id in [id for id in former_ids if id]:
            if former_id in self.id_map:
                del self.id_map[former_id]

    def create_entity(self, entity_mappings, marc_field, entity_parent_key, index_or_legacy_id):
        entity = {}
        for entity_mapping in entity_mappings:
            k = entity_mapping["target"].split(".")[-1]
            if values := self.apply_rules(marc_field, entity_mapping, index_or_legacy_id):
                if entity_parent_key == k:
                    entity = values[0]
                else:
                    entity[k] = values[0]
        return entity

    def handle_entity_mapping(
        self,
        marc_field,
        mapping,
        folio_record,
        legacy_ids,
    ):
        entity_mapping = mapping["entity"]
        e_parent = entity_mapping[0]["target"].split(".")[0]
        if mapping.get("entityPerRepeatedSubfield", False):
            for temp_field in self.grouped(marc_field):
                entity = self.create_entity(entity_mapping, temp_field, e_parent, legacy_ids)
                if (type(entity) is dict and all(entity.values())) or (
                    type(entity) is list and all(entity)
                ):
                    self.add_entity_to_record(entity, e_parent, folio_record, self.schema)
        else:
            if mapping.get("ignoreSubsequentSubfields", False):
                marc_field = self.remove_repeated_subfields(marc_field)
            entity = self.create_entity(entity_mapping, marc_field, e_parent, legacy_ids)
            if e_parent in ["precedingTitles", "succeedingTitles"]:
                self.create_preceding_succeeding_titles(entity, e_parent, folio_record["id"])
            elif (
                all(
                    v
                    for k, v in entity.items()
                    if k
                    not in [
                        "staffOnly",
                        "primary",
                        "isbnValue",
                        "issnValue",
                        "authorityId",
                    ]
                )
                or e_parent in ["electronicAccess", "publication"]
                or (
                    e_parent.startswith("holdingsStatements") and any(v for k, v in entity.items())
                )
            ):
                self.add_entity_to_record(entity, e_parent, folio_record, self.schema)
            else:
                sfs = " - ".join(
                    f"{f[0]}:{('has_value' if f[1].strip() else 'empty')}" for f in marc_field
                )
                pattern = " - ".join(f"{k}:'{bool(v)}'" for k, v in entity.items())
                self.migration_report.add(
                    Blurbs.IncompleteEntityMapping,
                    f"{marc_field.tag} {sfs} ->>-->> {e_parent} {pattern}  ",
                )
                # Experimental
                # self.add_entity_to_record(entity, e_parent, rec, self.schema)

    def handle_suppression(
        self, folio_record, file_def: FileDefinition, only_discovery_suppress: bool = False
    ):
        folio_record["discoverySuppress"] = file_def.suppressed
        self.migration_report.add(
            Blurbs.Suppression,
            f'Suppressed from discovery = {folio_record["discoverySuppress"]}',
        )
        if not only_discovery_suppress:
            folio_record["staffSuppress"] = file_def.staff_suppressed
            self.migration_report.add(
                Blurbs.Suppression, f'Staff suppressed = {folio_record["staffSuppress"]} '
            )

    def create_preceding_succeeding_titles(self, entity, e_parent, identifier):
        self.migration_report.add(Blurbs.PrecedingSuccedingTitles, f"{e_parent} created")
        # TODO: Make these uuids deterministic
        new_entity = {
            "id": str(uuid.uuid4()),
            "title": entity.get("title"),
            "identifiers": [],
        }
        if e_parent == "precedingTitles":
            new_entity["succeedingInstanceId"] = identifier
        else:
            new_entity["precedingInstanceId"] = identifier
        if new_entity.get("isbnValue", ""):
            new_entity["identifiers"].append(
                {
                    "identifierTypeId": new_entity.get("isbnId"),
                    "value": new_entity.get("isbnValue"),
                }
            )
        if new_entity.get("issnValue", ""):
            new_entity["identifiers"].append(
                {
                    "identifierTypeId": new_entity.get("issnId"),
                    "value": new_entity.get("issnValue"),
                }
            )
        self.extradata_writer.write(e_parent, new_entity)

    def apply_rule(self, legacy_id, value, condition_types, marc_field, parameter):
        v = value
        for condition_type in iter(condition_types):
            try:
                v = self.conditions.get_condition(
                    condition_type, legacy_id, v, parameter, marc_field
                )
            except AttributeError as attr_error:
                raise TransformationProcessError(
                    legacy_id, attr_error, condition_type
                ) from attr_error
        return v

    @staticmethod
    def add_entity_to_record(entity, entity_parent_key, rec, schema):
        sch = schema["properties"]
        if sch[entity_parent_key]["type"] == "array":
            if entity_parent_key not in rec:
                rec[entity_parent_key] = [entity]
            else:
                rec[entity_parent_key].append(entity)
        else:
            rec[entity_parent_key] = entity

    @staticmethod
    def grouped(marc_field: Field):
        """Groups the subfields
        s -> (s0,s1,s2,...sn-1), (sn,sn+1,sn+2,...s2n-1), (s2n,s2n+1,s2n+2,...s3n-1), ...


        Args:
            marc_field (Field): _description_

        Returns:
            _type_: _description_
        """
        unique_subfields: list = []
        repeated_subfields: list = []
        results = []
        for sf, sf_vals in marc_field.subfields_as_dict().items():
            if len(sf_vals) == 1:
                unique_subfields.extend([sf, sf_vals[0]])
            else:
                repeated_subfields.extend([sf, sf_val] for sf_val in sf_vals)
        if any(repeated_subfields):
            for repeated_subfield in repeated_subfields:
                new_subfields = [repeated_subfield[0], repeated_subfield[1], *unique_subfields]
                temp_field = Field(
                    tag=marc_field.tag,
                    indicators=marc_field.indicators,
                    subfields=new_subfields,
                )
                results.append(temp_field)
        else:
            temp_field = Field(
                tag=marc_field.tag,
                indicators=marc_field.indicators,
                subfields=unique_subfields,
            )
            results.append(temp_field)
        return results

    @staticmethod
    def remove_repeated_subfields(marc_field: Field):
        """Removes repeated subfields
        s -> (s0,s1,s2,...sn-1), (sn,sn+1,sn+2,...s2n-1), (s2n,s2n+1,s2n+2,...s3n-1), ...

        Args:
            marc_field (Field): _description_

        Returns:
            _type_: _description_
        """
        new_subfields = []
        for sf, sf_vals in marc_field.subfields_as_dict().items():
            new_subfields.extend([sf, sf_vals[0]])
        return Field(
            tag=marc_field.tag,
            indicators=marc_field.indicators,
            subfields=new_subfields,
        )

    @staticmethod
    def save_source_record(
        srs_records_file,
        record_type: FOLIONamespaces,
        folio_client: FolioClient,
        marc_record: Record,
        folio_record,
        legacy_ids: List[str],
        suppress: bool,
    ):
        """Saves the source Marc_record to the Source record Storage module

        Args:
            srs_records_file (_type_): _description_
            record_type (FOLIONamespaces): _description_
            folio_client (FolioClient): _description_
            marc_record (Record): _description_
            folio_record (_type_): _description_
            legacy_ids (List[str]): _description_
            suppress (bool): _description_
        """
        srs_id = RulesMapperBase.create_srs_id(record_type, folio_client.okapi_url, legacy_ids[-1])

        marc_record.add_ordered_field(
            Field(
                tag="999",
                indicators=["f", "f"],
                subfields=["i", folio_record["id"], "s", srs_id],
            )
        )
        # Since they all should be UTF encoded, make the leader align.
        try:
            temp_leader = Leader(marc_record.leader)
            temp_leader[9] = "a"
            marc_record.leader = temp_leader
        except Exception:
            logging.exception(
                "Something is wrong with the marc records leader: %s",
                marc_record.leader,
            )
        srs_record_string = RulesMapperBase.get_srs_string(
            marc_record,
            folio_record,
            srs_id,
            folio_client.get_metadata_construct(),
            suppress,
            record_type,
        )
        srs_records_file.write(f"{srs_record_string}\n")

    @staticmethod
    def create_srs_id(record_type, okapi_url: str, legacy_id: str):
        srs_types = {
            FOLIONamespaces.holdings: FOLIONamespaces.srs_records_holdingsrecord,
            FOLIONamespaces.instances: FOLIONamespaces.srs_records_bib,
            FOLIONamespaces.authorities: FOLIONamespaces.srs_records_auth,
            FOLIONamespaces.edifact: FOLIONamespaces.srs_records_edifact,
        }

        return str(FolioUUID(okapi_url, srs_types.get(record_type), legacy_id))

    @staticmethod
    def get_bib_id_from_907y(marc_record: Record, index_or_legacy_id):
        try:
            return list(set(marc_record["907"].get_subfields("a", "y")))
        except Exception as e:
            raise TransformationRecordFailedError(
                index_or_legacy_id,
                (
                    "907 $y and $a is missing is missing, although they is "
                    "required for this legacy ILS choice"
                ),
                marc_record.as_json(),
            ) from e

    @staticmethod
    def get_bib_id_from_990a(marc_record: Record, index_or_legacy_id):
        res = {f["a"].strip() for f in marc_record.get_fields("990") if "a" in f}
        if marc_record["001"].format_field().strip():
            res.add(marc_record["001"].format_field().strip())
        if any(res):
            return list(res)
        else:
            raise TransformationRecordFailedError(
                index_or_legacy_id,
                "neither 990$a or 001 found in record.",
                marc_record.as_json(),
            )

    @staticmethod
    def get_bib_id_from_001(marc_record: Record, index_or_legacy_id):
        try:
            return [marc_record["001"].format_field().strip()]
        except Exception as e:
            raise TransformationRecordFailedError(
                index_or_legacy_id,
                "001 is missing, although it is required for Voyager migrations",
                marc_record.as_json(),
            ) from e

    @staticmethod
    def get_srs_string(
        marc_record: Record,
        folio_object: dict,
        srs_id,
        metadata_obj,
        discovery_suppress: bool,
        record_type: FOLIONamespaces,
    ):
        record_types = {
            FOLIONamespaces.holdings: "MARC_HOLDING",
            FOLIONamespaces.instances: "MARC_BIB",
            FOLIONamespaces.authorities: "MARC_AUTHORITY",
            FOLIONamespaces.edifact: "EDIFACT",
        }

        id_holders = {
            FOLIONamespaces.instances: {
                "instanceId": folio_object["id"],
                "instanceHrid": folio_object.get("hrid", ""),
            },
            FOLIONamespaces.holdings: {
                "holdingsId": folio_object["id"],
                "holdingsHrid": folio_object.get("hrid", ""),
            },
            FOLIONamespaces.authorities: {
                "authorityId": folio_object["id"],
                "authorityHrid": marc_record["001"].data,
            },
            FOLIONamespaces.edifact: {},
        }

        my_tuple_json = marc_record.as_json()
        raw_record = {"id": srs_id, "content": my_tuple_json}
        parsed_record = {"id": srs_id, "content": json.loads(my_tuple_json)}
        record = {
            "id": srs_id,
            "deleted": False,
            "matchedId": srs_id,
            "generation": 0,
            "recordType": record_types.get(record_type),
            "rawRecord": raw_record,
            "parsedRecord": parsed_record,
            "additionalInfo": {"suppressDiscovery": discovery_suppress},
            "externalIdsHolder": id_holders.get(record_type),
            "metadata": metadata_obj,
            "state": "ACTUAL",
            "leaderRecordStatus": parsed_record["content"]["leader"][5]
            if parsed_record["content"]["leader"][5] in [*"acdnposx"]
            else "d",
        }
        return json.dumps(record)


def has_conditions(mapping):
    return mapping.get("rules", []) and mapping["rules"][0].get("conditions", [])


def has_value_to_add(mapping):
    return mapping.get("rules", []) and mapping["rules"][0].get("value", "")


def is_array_of_strings(schema_property):
    sc_prop_type = schema_property.get("type", "string")
    return sc_prop_type == "array" and schema_property["items"]["type"] == "string"


def is_array_of_objects(schema_property):
    sc_prop_type = schema_property.get("type", "string")
    return sc_prop_type == "array" and schema_property["items"]["type"] == "object"
