from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy

from iondb.rundb import models
import types
import datetime
import time
import logging

import re

from iondb.rundb.models import (
    SampleGroupType_CV,
    SampleAnnotation_CV,
    SampleSet,
    SampleSetItem,
    Sample,
    SampleAttribute,
    SampleAttributeValue,
    common_CV,
    KitInfo,
)
from iondb.rundb.labels import (
    Sample as _Sample,
    SampleAttribute as _SampleAttribute,
    SampleSet as _SampleSet,
    SampleSetItem as _SampleSetItem,
)
from iondb.utils import validation
from iondb.rundb.plan.plan_validator import (
    validate_sampleControlType,
    validate_optional_kit_name,
)
from iondb.utils.verify_types import (
    RepresentsInt,
    RepresentsUnsignedInt,
    RepresentsUnsignedIntOrZero,
)

logger = logging.getLogger(__name__)

MAX_LENGTH_SAMPLE_NAME = 127
MAX_LENGTH_SAMPLE_DISPLAYED_NAME = 127
MAX_LENGTH_SAMPLE_EXTERNAL_ID = 127
MAX_LENGTH_SAMPLE_DESCRIPTION = 1024

MAX_LENGTH_SAMPLE_SET_DISPLAYED_NAME = 127
MAX_LENGTH_SAMPLE_SET_DESCRIPTION = 1024

MAX_LENGTH_SAMPLE_ATTRIBUTE_DISPLAYED_NAME = 127
MAX_LENGTH_SAMPLE_ATTRIBUTE_DESCRIPTION = 1024

MAX_LENGTH_SAMPLE_ATTRIBUTE_VALUE = SampleAttributeValue._meta.get_field(
    "value"
).max_length

MAX_LENGTH_PCR_PLATE_SERIAL_NUM = 64
MAX_LENGTH_SAMPLE_CELL_NUM = SampleSetItem._meta.get_field("cellNum").max_length
MAX_LENGTH_SAMPLE_COUPLE_ID = SampleSetItem._meta.get_field("coupleId").max_length
MAX_LENGTH_SAMPLE_EMBRYO_ID = SampleSetItem._meta.get_field("cellNum").max_length
SC_DATE_FORMAT = "YYYY/MM/DD"
ASHD_SAMPLESET_ITEM_LIMIT = 8

VALID_LIB_TYPES = ["amps_on_chef_v1", "amps_hd_on_chef_v1"] # some validations are applicable only for these types

def validate_sampleSet(queryDict, isNew=False, sampleset_label=_SampleSet.verbose_name):
    """
    validate the sampleSet input.
    returns a boolean isValid and a text string for error message, None if input passes validation
    Note: Input length will not be validated since maxLength has been specified in the form.
    """

    logger.debug("sample_validator.validate_sampleset() queryDict=%s" % (queryDict))

    if not queryDict:
        return (
            False,
            validation.invalid_no_data(sampleset_label, include_error_prefix=True),
        )

    sampleSetName = queryDict.get("sampleSetName", "").strip()
    sampleSetDesc = queryDict.get("sampleSetDescription", "").strip()
    pcrPlateSerialNum = queryDict.get("pcrPlateSerialNum", "").strip()
    sampleGroupTypeName = queryDict.get("sampleGroupTypeName", "").strip()
    additionalCycles = queryDict.get("additionalCycles", "").strip()
    libraryPrepProtocol = queryDict.get("libraryPrepProtocol", "").strip()
    libraryPrepKit = queryDict.get("libraryPrepKit", "").strip()

    return validate_sampleSet_values(
        sampleSetName, sampleSetDesc, pcrPlateSerialNum, isNew, sampleGroupTypeName, additionalCycles,
        libraryPrepProtocol, libraryPrepKit,
    )

def validate_sampleSet_values(sampleSetName, sampleSetDesc, pcrPlateSerialNum, isNew=False,
        sampleGroupTypeName=None, additionalCycles=None, libraryPrepProtocol=None, libraryPrepKit=None
):
    """
    validate the sampleSet input.
    returns a boolean isValid and a text string for error message, None if input passes validation
    Note: Input length willl not be validated since maxLength has been specified in the form.
    """
    if not validation.has_value(sampleSetName):
        return (
            False,
            validation.required_error(
                ugettext_lazy("samplesets.fields.displayedName.label"),
                include_error_prefix=True,
            ),
        )  # "Sample set name"
    else:
        if not validation.is_valid_chars(sampleSetName):
            return (
                False,
                validation.invalid_chars_error(
                    ugettext_lazy("samplesets.fields.displayedName.label"),
                    include_error_prefix=True,
                ),
            )

        if not validation.is_valid_length(
            sampleSetName, MAX_LENGTH_SAMPLE_SET_DISPLAYED_NAME
        ):
            errorMessage = validation.invalid_length_error(
                ugettext_lazy("samplesets.fields.displayedName.label"),
                MAX_LENGTH_SAMPLE_SET_DISPLAYED_NAME,
                sampleSetName.strip(),
                include_error_prefix=True,
            )
            return False, errorMessage

        if isNew:
            # error if new sample set already exists
            existingSampleSets = SampleSet.objects.filter(displayedName=sampleSetName)
            if existingSampleSets:
                errorMessage = validation.invalid_entity_field_unique(
                    _SampleSet.verbose_name,
                    ugettext_lazy("samplesets.fields.displayedName.label"),
                    include_error_prefix=True,
                )
                return False, errorMessage

    if validation.has_value(sampleSetDesc):
        if not validation.is_valid_chars(sampleSetDesc):
            return (
                False,
                validation.invalid_chars_error(
                    ugettext_lazy("samplesets.fields.description.label"),
                    include_error_prefix=True,
                ),
            )

        if not validation.is_valid_length(
            sampleSetDesc, MAX_LENGTH_SAMPLE_SET_DESCRIPTION
        ):
            errorMessage = validation.invalid_length_error(
                ugettext_lazy("samplesets.fields.description.label"),
                MAX_LENGTH_SAMPLE_SET_DESCRIPTION,
                sampleSetDesc.strip(),
                include_error_prefix=True,
            )
            return False, errorMessage

    if validation.has_value(pcrPlateSerialNum):
        if not validation.is_valid_chars(pcrPlateSerialNum):
            return (
                False,
                validation.invalid_chars_error(
                    ugettext_lazy("samplesets.fields.pcrPlateSerialNum.label"),
                    include_error_prefix=True,
                ),
            )  # Sample set PCR plate serial number

        if not validation.is_valid_length(
            pcrPlateSerialNum, MAX_LENGTH_PCR_PLATE_SERIAL_NUM
        ):
            errorMessage = validation.invalid_length_error(
                ugettext_lazy("samplesets.fields.pcrPlateSerialNum.label"),
                MAX_LENGTH_PCR_PLATE_SERIAL_NUM,
                pcrPlateSerialNum.strip(),
                include_error_prefix=True,
            )  # Sample PCR plate serial number
            return False, errorMessage

    if validation.has_value(sampleGroupTypeName):
        isValid, errorMessage = validate_samplesetGroupType(
            sampleGroupTypeName, field_label="sampleGroupTypeName"
        )
        if not isValid:
            return isValid, errorMessage

    if validation.has_value(additionalCycles):

        isValid, errorMessage, cvValue = validate_additionalCycles(
            additionalCycles, field_label="additionalCycles"
        )
        if not isValid:
            return isValid, errorMessage

    if validation.has_value(libraryPrepProtocol):
        isValid, errorMessage, cvValue = validate_libraryPrepProtocol(
            libraryPrepProtocol, field_label="libraryPrepProtocol"
        )
        if not isValid:
            return isValid, errorMessage
        else:
            errorMessage = validate_libraryPrepProtocol_for_kit(libraryPrepProtocol, libraryPrepKit)
            if errorMessage:
                return False, errorMessage

    return True, None

def validate_barcoding_samplesetitems(
    samplesetitems,
    barcodeKit,
    barcode,
    samplesetitem_id,
    allPcrPlates=None,
    pcrPlateRow=None,
):
    """
        1) validate only one barcodeKit is used for all barcode items, no barcode seletion is ok
        2) validate barcode selected for current SampleSetItem (given by samplesetitem_id) is unique or None
        3) special logic when this function is used by Chef validation (pcrPlateRow is specified)

        If validating with a dict of samplesetitems, "pending_id" key must contain a unique id number.
    """
    if not barcodeKit and not barcode:
        return True, None

    for item in samplesetitems:
        if type(item) == types.DictType:
            id1 = item.get("pending_id")
            barcodeKit1 = item.get("barcodeKit", barcodeKit)
            barcode1 = item.get("barcode", None)
            item_name = item.get("name", None)
        else:
            id1 = item.pk
            barcode1 = item.dnabarcode.id_str if item.dnabarcode else None
            barcodeKit1 = item.dnabarcode.name if item.dnabarcode else None
            item_name = item.sample.name

        # skip self
        if id1 and samplesetitem_id and int(id1) == int(samplesetitem_id):
            continue

        if barcodeKit and barcodeKit1 and barcodeKit != barcodeKit1:
            if not pcrPlateRow:
                errorMessage = validation.format(
                    ugettext_lazy(
                        "samplesets.samplesetitem.messages.validate.barcodekit.notequal"
                    ),
                    include_error_prefix=True,
                )  # "Error, Only one barcode kit can be used for a sample set"
                return False, errorMessage

        # ensure only 1 barcode id_str per sample
        # TODO: Make sure CSV and API plan creation works as expected
        if pcrPlateRow and barcode and barcode1 and barcode == barcode1:
            # TODO : change the localization key field to sample name
            # errorMessage = validation.format(ugettext_lazy('samplesets.samplesetitem.messages.validate.barcode.unique.withplateposition'), {'barcode': barcode, 'pcrPlateRow': item_name}, include_error_prefix=True)  # "Error, A barcode can be assigned to only one sample in the sample set. %s has been assigned to another sample at PCR plate position (%s)" % (barcode, item_pcrPlate)
            errorMessage = (
                "Error, A barcode can be assigned to only one sample in the sample set. %s has been assigned to another sample (%s)"
                % (barcode, item_name)
            )
            if item.pcrPlateRow not in allPcrPlates:
                return False, errorMessage
            else:
                return True, ""
            return False, errorMessage
    return True, None

"""
    If User select "Ampliseq HD on chef" ->
        - validate for 4 samples in each Chef assay group i.e 4 samples in group1 and 4 samples in group2
        - make sure each assay group belongs to one category(nuc. type + pool type + sample source ) respectively
         - If validation is successfull, tube position and pcr plate assignment operation will be performed in views.save_input_samples_for_sampleset()
         - Detailed validation and design logic @ https://confluence.amer.thermo.com/display/TS/Tech+Design+proposal+-+Ampliseq+HD+on+chef+support
    isAmpliseqHD : "is Ampliseq or Ampliseq HD". This flag is used by API/CSV to parse all the items
"""

def validate_inconsistent_ampliseq_HD_category(
    samplesetitems, pending_sampleSetItem_list, sampleset=None, isAmpliseqHD=False
):
    isValid = True
    errorMessage = []
    categoryDict = {}
    no_of_partitionKey = None
    if (
        sampleset and "amps_hd_on_chef_v1" in sampleset.libraryPrepType.lower()
    ) or isAmpliseqHD:
        if not samplesetitems:
            samplesetitems = pending_sampleSetItem_list
        try:
            samplesetitems.reverse()
            for item in samplesetitems:
                if type(item) == types.DictType:
                    nucleotideType = item.get("nucleotideType", "")
                    sampleSource = item.get("sampleSource", "")
                    panelPoolType = item.get("panelPoolType", "")
                else:
                    nucleotideType = item.nucleotideType
                    sampleSource = item.sampleSource
                    panelPoolType = item.panelPoolType

                if not nucleotideType or not sampleSource or not panelPoolType:
                    errorMessage.append(
                        "Error, Missing values: NucleotideType, SampleSource and panelPoolType must be specified for AmpliseSeq HD on chef sample"
                    )
                    isValid = False
                category = "{0}_{1}_{2}".format(
                    nucleotideType, sampleSource, panelPoolType
                )
                if not categoryDict:
                    categoryDict[category] = {"Group 1": [item]}
                    continue
                else:
                    existingGroups = [
                        list(groups.keys()) for cat, groups in categoryDict.items()
                    ]
                    flattened_groups = [y for x in existingGroups for y in x]
                    if (
                        "Group 2" not in flattened_groups
                        and category not in categoryDict
                    ):
                        categoryDict[category] = {"Group 2": [item]}
                        continue

                if category not in categoryDict:
                    errorMessage.append(
                        "Error, Too many assay group categories (Nucleotide type + Sample source + Panel pool type) specified for AmpliSeq HD on Chef sample. Only 2 assay groups are allowed."
                    )
                    return False, errorMessage, categoryDict
                if "Group 1" in categoryDict[category]:
                    categoryDict[category]["Group 1"].append(item)
                else:
                    categoryDict[category]["Group 2"].append(item)

            if categoryDict:
                no_of_partitionKey = len(list(categoryDict.keys()))
        except Exception as Err:
            logger.debug(Err)
            errorMessage.append(Err)
        if no_of_partitionKey > 2:
            errorMessage.append(
                "Error, Too many assay group category (NucleotideType + Sample source + panel pool type) specified for AmpliSeq HD on Chef sample"
            )
            isValid = False
        if categoryDict:
            for partitionKey, groups in categoryDict.items():
                for key, value in groups.items():
                    if len(value) > 4:
                        errorMessage.append(
                            "Error, Only 4 samples are allowed in each Assay group categories (Nucleotide type + Sample source + Pool type)"
                        )
                        isValid = False

    return isValid, errorMessage, categoryDict

def validate_pcrPlate_position_samplesetitems(
    samplesetitems, pcrPlateRow, samplesetitem_id, sampleset=None
):
    """
        1) validate pcrPlateRow position selected for sampleSetItem is unique
        2) for Ampliseq on Chef pcrPlateRow value is required

        If validating with a dict of samplesetitems, "pending_id" key must contain a unique id number.
    """
    for item in samplesetitems:
        if (
            sampleset
            and not pcrPlateRow
            and "amps_on_chef" in sampleset.libraryPrepType.lower()
        ):
            errorMessage = validation.format(
                ugettext_lazy(
                    "samplesets.samplesetitem.messages.validate.pcrPlateRow.required"
                ),
                include_error_prefix=True,
            )  # "Error, A PCR plate position must be specified for AmpliSeq on Chef sample"
            return False, errorMessage

        if type(item) == types.DictType:
            pcrPlateRow1 = item.get("pcrPlateRow", "")
            id1 = item.get("pending_id")
        else:
            pcrPlateRow1 = item.pcrPlateRow
            id1 = item.pk

        # skip self
        if id1 and samplesetitem_id and int(id1) == int(samplesetitem_id):
            continue

        # ensure only 1 pcr plate position per sample
        if pcrPlateRow and pcrPlateRow1 and pcrPlateRow.lower() == pcrPlateRow1.lower():
            errorMessage = validation.format(
                ugettext_lazy(
                    "samplesets.samplesetitem.messages.validate.pcrPlateRow.unique"
                ),
                {"pcrPlateRow": pcrPlateRow},
                include_error_prefix=True,
            )  # "Error, A PCR plate position can only have one sample in it. Position %s has already been occupied by another sample" % (pcrPlateRow)
            return False, errorMessage

    return True, None

def validate_sample_for_sampleSet(
    queryDict, samplesetitem_label=_SampleSetItem.verbose_name
):
    """
    basic validation of sample attributes for sample set item creation/update
    return a boolean isValid and a list of text error messages
    """
    errors = []

    if not queryDict:
        return (
            False,
            [
                validation.invalid_no_data(
                    samplesetitem_label, include_error_prefix=True
                )
            ],
        )  # "Error, No sample data to validate."

    # validate sample fields

    sample_errors = validate_sample_data(queryDict)
    if sample_errors:
        errors.extend(sample_errors)

    # validate sampleset item fields

    isValid, errorMessage, _ = validate_nucleotideType(
        queryDict.get("nucleotideType", "").strip()
    )
    if not isValid:
        errors.append(errorMessage)

    isValid, errorMessage, _ = validate_sampleSource(
        queryDict.get("sampleSource", "").strip()
    )
    if not isValid:
        errors.append(errorMessage)

    isValid, errorMessage, _ = validate_panelPoolType(
        queryDict.get("panelPoolType", "").strip()
    )
    if not isValid:
        errors.append(errorMessage)

    isValid, errorMessage, _ = validate_sampleGender(
        queryDict.get("gender", "").strip()
    )
    if not isValid:
        errors.append(errorMessage)

    isValid, errorMessage, _ = validate_controlType(
        queryDict.get("controlType", "").strip()
    )
    if not isValid:
        errors.append(errorMessage)

    isValid, errorMessage, _ = validate_sampleGroupType(
        queryDict.get("relationshipRole", "").strip()
    )
    if not isValid:
        errors.append(errorMessage)

    isValid, errorMessage = validate_sampleGroup(queryDict.get("relationshipGroup", ""))
    if not isValid:
        errors.append(errorMessage)

    isValid, errorMessage, _ = validate_cancerType(
        queryDict.get("cancerType", "").strip()
    )
    if not isValid:
        errors.append(errorMessage)

    isValid, errorMessage = validate_cellularityPct(queryDict.get("cellularityPct"))
    if not isValid:
        errors.append(errorMessage)

    isValid, errorMessage = validate_sampleBiopsyDays(queryDict.get("biopsyDays", ""))
    if not isValid:
        errors.append(errorMessage)

    isValid, errorMessage = validate_sampleCoupleId(
        queryDict.get("coupleId", "").strip()
    )
    if not isValid:
        errors.append(errorMessage)

    isValid, errorMessage = validate_sampleCellNum(queryDict.get("cellNum", "").strip())
    if not isValid:
        errors.append(errorMessage)

    isValid, errorMessage = validate_sampleEmbryoId(
        queryDict.get("embryoId", "").strip()
    )
    if not isValid:
        errors.append(errorMessage)

    isValid, errorMessage, _ = validate_population(
        queryDict.get("population", "").strip()
    )
    if not isValid:
        errors.append(errorMessage)

    isValid, errorMessage, _ = validate_mouseStrains(
        queryDict.get("mouseStrains", "").strip()
    )
    if not isValid:
        errors.append(errorMessage)

    sampleCollectionDate = queryDict.get("sampleCollectionDate", "").strip()
    isValid, errorMessage = validate_sampleCollectionDate(sampleCollectionDate)
    if not isValid:
        errors.append(errorMessage)

    sampleReceiptDate = queryDict.get("sampleReceiptDate", "").strip()
    isValid, errorMessage = validate_sampleReceiptDate(
        sampleReceiptDate, sampleCollectionDate
    )
    if not isValid:
        errors.append(errorMessage)

    return len(errors) == 0, errors

def validate_sampleDisplayedName(
    sampleDisplayedName, field_label=_Sample.displayedName.verbose_name
):
    isValid, errorMessage = _validate_textValue_mandatory(
        sampleDisplayedName, field_label
    )

    if not isValid:
        return False, errorMessage

    isValid, errorMessage = _validate_textValue(sampleDisplayedName, field_label)

    if not isValid:
        return False, errorMessage

    isValid, errorMessage = _validate_textValue_leadingChars(
        sampleDisplayedName, field_label
    )

    if not isValid:
        return False, errorMessage

    if not validation.is_valid_length(
        sampleDisplayedName.strip(), MAX_LENGTH_SAMPLE_DISPLAYED_NAME
    ):
        errorMessage = validation.invalid_length_error(
            field_label,
            MAX_LENGTH_SAMPLE_DISPLAYED_NAME,
            sampleDisplayedName.strip(),
            include_error_prefix=True,
        )
        return False, errorMessage

    return True, None

def validate_sampleExternalId(
    sampleExternalId, field_label=_Sample.externalId.verbose_name
):
    isValid = False
    isValid, errorMessage = _validate_textValue(sampleExternalId, field_label)

    if not isValid:
        return False, errorMessage

    if not validation.is_valid_length(
        sampleExternalId.strip(), MAX_LENGTH_SAMPLE_EXTERNAL_ID
    ):
        errorMessage = validation.invalid_length_error(
            field_label,
            MAX_LENGTH_SAMPLE_EXTERNAL_ID,
            sampleExternalId.strip(),
            include_error_prefix=True,
        )
        return False, errorMessage

    return True, None

def validate_sampleDescription(
    sampleDescription, field_label=_Sample.description.verbose_name
):
    isValid = False
    if validation.has_value(sampleDescription):
        isValid, errorMessage = _validate_textValue(sampleDescription, field_label)
        if not isValid:
            return False, errorMessage

        if not validation.is_valid_length(
            sampleDescription.strip(), MAX_LENGTH_SAMPLE_DESCRIPTION
        ):
            errorMessage = validation.invalid_length_error(
                field_label,
                MAX_LENGTH_SAMPLE_DESCRIPTION,
                sampleDescription.strip(),
                include_error_prefix=True,
            )
            return False, errorMessage

    return True, None

def _validate_SampleAnnotation_CV(cvValue, cvAnnotationType, field_label):
    if not cvValue:
        return True, None, cvValue

    cvs = SampleAnnotation_CV.objects.filter(
        annotationType=cvAnnotationType, isActive=True
    )
    cvsMatching = cvs.filter(value__iexact=cvValue)

    isValid = False
    if cvsMatching.count() == 0:
        choices = cvs.order_by("value").values_list("value", flat=True)
        return (
            False,
            validation.invalid_choice(
                field_label, cvValue, choices, include_error_prefix=True
            ),
            cvValue,
        )

    return True, None, cvsMatching[0]

def validate_sampleSource(sampleSource, field_label="Sample Source"):
    return _validate_SampleAnnotation_CV(sampleSource, "sampleSource", field_label)

def validate_panelPoolType(panelPoolType, field_label="Panel Pool Type"):
    return _validate_SampleAnnotation_CV(panelPoolType, "panelPoolType", field_label)

def validate_additionalCycles(additionalCycles, field_label="Additional Cycles"):
    return _validate_SampleAnnotation_CV(
        additionalCycles, "additionalCycles", field_label
    )

def validate_libraryPrepProtocol(libraryPrepProtocol, field_label="Library Prep Protocol"):
    value = libraryPrepProtocol
    isValid = True
    error = None

    if libraryPrepProtocol:
        choices = common_CV.objects.filter(cv_type="libraryPrepProtocol", isActive=True)
        match = choices.filter(value__iexact=libraryPrepProtocol)
        if match:
            value = match[0].value
        else:
            isValid = False
            error = validation.invalid_choice(
                field_label,
                libraryPrepProtocol,
                choices.order_by("value").values_list("value", flat=True),
                include_error_prefix=True,
            )

    return isValid, error, value

def validate_libraryPrepProtocol_for_kit(libraryPrepProtocol, libraryPrepKit,
                                        field_label="Library Prep Protocol", kit_label="Library Prep Kit"):
    errors = []
    if libraryPrepProtocol:
        protocol = common_CV.objects.filter(value=libraryPrepProtocol)
        kit = KitInfo.objects.filter(name=libraryPrepKit)
        if protocol and kit and protocol[0].categories:
            if not any(c in kit[0].categories.split(";") for c in protocol[0].categories.split(";")):
                errors.append(
                    validation.invalid_invalid_value_related_value(
                        field_label,
                        protocol[0].displayedValue,
                        kit_label,
                        kit[0].description,
                    )
                )
    return errors

def validate_sampleGender(
    sampleGender,
    field_label=ugettext_lazy("samplesets.samplesetitem.fields.gender.label"),
):
    return _validate_SampleAnnotation_CV(sampleGender, "gender", field_label)

def validate_sampleGroupType(
    sampleGroupType,
    field_label=ugettext_lazy("samplesets.samplesetitem.fields.relationshipRole.label"),
):
    return _validate_SampleAnnotation_CV(
        sampleGroupType, "relationshipRole", field_label
    )

def validate_sampleGroup(
    sampleGroup,
    field_label=ugettext_lazy(
        "samplesets.samplesetitem.fields.relationshipGroup.label"
    ),
):
    if sampleGroup and not sampleGroup.isdigit():
        return False, validation.invalid_uint(field_label, include_error_prefix=True)

    return True, None

def validate_cancerType(
    cancerType,
    field_label=ugettext_lazy("samplesets.samplesetitem.fields.cancerType.label"),
):
    return _validate_SampleAnnotation_CV(cancerType, "cancerType", field_label)

def validate_population(population, field_label="Population"):
    return _validate_SampleAnnotation_CV(population, "population", field_label)

def validate_mouseStrains(mouseStrains, field_label="Mouse Strains"):
    return _validate_SampleAnnotation_CV(mouseStrains, "mouseStrains", field_label)

def validate_cellularityPct(
    cellularityPct,
    field_label=ugettext_lazy("samplesets.samplesetitem.fields.cellularityPct.label"),
):
    """
    check if input is a positive integer between 0 and 100 inclusively.
    """

    if cellularityPct:
        if RepresentsInt(cellularityPct):
            value = int(cellularityPct)
            if value < 0:
                return (
                    False,
                    validation.invalid_min_value(field_label, 0, include_error_prefix=True),
                )
            elif value > 100:
                return (
                    False,
                    validation.invalid_max_value(
                        field_label, 100, include_error_prefix=True
                    ),
                )
            else:
                return True, None
        else:
            return (
                False,
                validation.invalid_uint(field_label, include_error_prefix=True),
            )

    return True, None

def validate_sampleCollectionDate(
    sampleCollectionDate,
    field_label=ugettext_lazy(
        "samplesets.samplesetitem.fields.sampleCollectionDate.label"
    ),
):
    if sampleCollectionDate:
        try:
            datetime.datetime.strptime(sampleCollectionDate, "%Y-%m-%d")
        except ValueError:
            return (
                False,
                validation.invalid_date_format(field_label, include_error_prefix=True),
            )

    return True, None

def validate_sampleReceiptDate(
    sampleReceiptDate,
    sampleCollectionDate,
    field_label=ugettext_lazy(
        "samplesets.samplesetitem.fields.sampleReceiptDate.label"
    ),
):
    if sampleReceiptDate:
        try:
            datetime.datetime.strptime(sampleReceiptDate, "%Y-%m-%d")
        except ValueError:
            return (
                False,
                validation.invalid_date_format(field_label, include_error_prefix=True),
            )
        if sampleCollectionDate:
            try:
                datetime.datetime.strptime(sampleCollectionDate, "%Y-%m-%d")
                sample_collectDate = time.strptime(sampleCollectionDate, "%Y-%m-%d")
                sample_receiptDate = time.strptime(sampleReceiptDate, "%Y-%m-%d")
                if sample_receiptDate and sample_receiptDate < sample_collectDate:
                    return (
                        False,
                        validation.invalid_receipt_date(
                            field_label, include_error_prefix=True
                        ),
                    )
            except ValueError:
                return (
                    False,
                    validation.invalid_receipt_date(
                        field_label, include_error_prefix=True
                    ),
                )

    return True, None

def validate_sampleBiopsyDays(
    sampleBiopsyDays,
    field_label=ugettext_lazy("samplesets.samplesetitem.fields.biopsyDays.label"),
):
    isValid, errorMessage = _validate_uintandzeroValue(sampleBiopsyDays, field_label)
    return isValid, errorMessage

def validate_sampleCellNum(
    sampleCellNum,
    field_label=ugettext_lazy("samplesets.samplesetitem.fields.cellNum.label"),
):
    return _validate_optional_text(
        sampleCellNum, MAX_LENGTH_SAMPLE_CELL_NUM, field_label
    )

def validate_sampleCoupleId(
    sampleCoupleId,
    field_label=ugettext_lazy("samplesets.samplesetitem.fields.coupleId.label"),
):
    return _validate_optional_text(
        sampleCoupleId, MAX_LENGTH_SAMPLE_COUPLE_ID, field_label
    )

def validate_sampleEmbryoId(
    sampleEmbryoId,
    field_label=ugettext_lazy("samplesets.samplesetitem.fields.embryoId.label"),
):
    return _validate_optional_text(
        sampleEmbryoId, MAX_LENGTH_SAMPLE_EMBRYO_ID, field_label
    )

def _validate_textValue_mandatory(value, displayedTerm):
    if not validation.has_value(value):
        return (
            False,
            validation.required_error(displayedTerm, include_error_prefix=True),
        )

    return True, None

def _validate_intValue(value, displayedTerm):
    if value and not RepresentsUnsignedInt(value):
        return (
            False,
            validation.invalid_uint(displayedTerm, include_error_prefix=True),
        )  # "Error, " + displayedTerm + ERROR_MSG_INVALID_DATATYPE
    return True, None

def _validate_uintandzeroValue(value, displayedTerm):
    if value and not RepresentsUnsignedIntOrZero(value):
        return (
            False,
            validation.invalid_uint_n_zero(displayedTerm, include_error_prefix=True),
        )  # "Error, " + displayedTerm + ERROR_MSG_INVALID_DATATYPE
    return True, None

def _validate_textValue(value, displayedTerm):
    if value and not validation.is_valid_chars(value):
        return (
            False,
            validation.invalid_chars_error(displayedTerm, include_error_prefix=True),
        )
    return True, None

def _validate_textValue_leadingChars(value, displayedTerm):
    if value and not validation.is_valid_leading_chars(value):
        return (
            False,
            validation.invalid_leading_chars(displayedTerm, include_error_prefix=True),
        )

    return True, None

def _validate_optional_text(value, maxLength, displayedTerm):
    isValid, errorMessage = _validate_textValue(value.strip(), displayedTerm)
    if not isValid:
        return False, errorMessage

    if not validation.is_valid_length(value.strip(), maxLength):
        errorMessage = validation.invalid_length_error(
            displayedTerm, maxLength, value.strip(), include_error_prefix=True
        )
        return False, errorMessage

    return True, None

def validate_sampleAttribute(
    attribute, value, sampleattribute_label=_SampleAttribute.verbose_name
):
    """
    validate the sample attribute value for the attribute of interest
    return a boolean isValid and a text string for error message, None if input passes validation
    Note: Input length will not be validated since maxLength has been specified in the form.
    """

    isValid = False
    if not attribute:
        return (
            False,
            validation.invalid_no_data(
                sampleattribute_label, include_error_prefix=True
            ),
        )  # "Error, No sample attribute to validate."

    if not validation.has_value(value):
        if attribute.isMandatory:
            return (
                False,
                validation.required_error(
                    attribute.displayedName, include_error_prefix=True
                ),
            )
    else:
        aValue = value.strip()
        if attribute.dataType.dataType == "Text" and not validation.is_valid_chars(
            aValue
        ):
            return (
                False,
                validation.invalid_chars_error(
                    attribute.displayedName, include_error_prefix=True
                ),
            )
        if attribute.dataType.dataType == "Integer" and not aValue.isdigit():
            return (
                False,
                validation.invalid_int(
                    attribute.displayedName, include_error_prefix=True
                ),
            )
        if not validation.is_valid_chars(aValue):
            return (
                False,
                validation.invalid_chars_error(
                    attribute.displayedName, include_error_prefix=True
                ),
            )

        if not validation.is_valid_length(aValue, MAX_LENGTH_SAMPLE_ATTRIBUTE_VALUE):
            errorMessage = validation.invalid_length_error(
                attribute.displayedName,
                MAX_LENGTH_SAMPLE_ATTRIBUTE_VALUE,
                aValue,
                include_error_prefix=True,
            )
            return False, errorMessage

    isValid = True
    return isValid, None

def validate_sampleAttribute_mandatory_for_no_value(
    attribute, sampleattribute_label=_SampleAttribute.verbose_name
):
    if not attribute:
        return (
            False,
            validation.invalid_no_data(
                sampleattribute_label, include_error_prefix=True
            ),
        )  # "Error, No sample attribute to validate."

    if attribute.isMandatory:
        return (
            False,
            validation.required_error(
                attribute.displayedName, include_error_prefix=True
            ),
        )  # "Error, " + attribute.displayedName + " value is required."

    return True, None

def validate_sampleAttribute_definition(
    attributeName,
    attributeDescription,
    attribute_name_label=ugettext_lazy(
        "samplesets.sampleattributes.fields.displayedName.label"
    ),
    attribute_description_label=ugettext_lazy(
        "samplesets.sampleattributes.fields.description.label"
    ),
):
    """
    validate the sample attribute definition
    return a boolean isValid and a text string for error message, None if input passes validation
    Note: Input length will not be validated since maxLength has been specified in the form.
    """

    if not validation.has_value(attributeName):
        return (
            False,
            validation.required_error(attribute_name_label, include_error_prefix=True),
        )
    if not validation.is_valid_chars(attributeName.strip()):
        return (
            False,
            validation.invalid_chars_error(
                attribute_name_label, include_error_prefix=True
            ),
        )

    if not validation.is_valid_length(
        attributeName.strip(), MAX_LENGTH_SAMPLE_ATTRIBUTE_DISPLAYED_NAME
    ):
        errorMessage = validation.invalid_length_error(
            attribute_name_label,
            MAX_LENGTH_SAMPLE_ATTRIBUTE_DISPLAYED_NAME,
            attributeName.strip(),
            include_error_prefix=True,
        )
        return False, errorMessage

    if not validation.is_valid_chars(attributeDescription):
        return (
            False,
            validation.invalid_chars_error(
                attribute_description_label, include_error_prefix=True
            ),
        )

    if not validation.is_valid_length(
        attributeDescription.strip(), MAX_LENGTH_SAMPLE_ATTRIBUTE_DESCRIPTION
    ):
        errorMessage = validation.invalid_length_error(
            attribute_description_label,
            MAX_LENGTH_SAMPLE_ATTRIBUTE_DESCRIPTION,
            attributeDescription.strip(),
            include_error_prefix=True,
        )
        return False, errorMessage

    return True, None

def validate_barcodekit_and_id_str(
    barcodeKit,
    barcode_id_str,
    barcodeKit_label="Barcode Set",
    barcode_id_str_label="Barcode",
):
    item = ""
    errorMessage = ""

    # Trim off barcode and barcode kit leading and trailing spaces and update the log file if exists
    if (len(barcodeKit) - len(barcodeKit.lstrip())) or (
        len(barcodeKit) - len(barcodeKit.rstrip())
    ):
        logger.warning(
            "The BarcodeKitName(%s) contains Leading/Trailing spaces and got trimmed."
            % barcodeKit
        )

    if (len(barcode_id_str) - len(barcode_id_str.lstrip())) or (
        len(barcode_id_str) - len(barcode_id_str.rstrip())
    ):
        logger.warning(
            "The BarcodeName (%s) of BarcodeKitName(%s) contains Leading/Trailing spaces and got trimmed."
            % (barcode_id_str, barcodeKit)
        )

    barcodeKit = barcodeKit.strip()
    barcode_id_str = barcode_id_str.strip()

    if not barcodeKit and not barcode_id_str:
        return True, errorMessage, item

    # First validate that if the barcodeKit is entered then the id_str must also be entered
    if barcodeKit and not barcode_id_str:
        return (
            False,
            validation.required_error(barcode_id_str_label, include_error_prefix=True),
            "barcode_id_str",
        )  # "Error, Please enter a barcode item", 'barcode_id_str'
    # Next validate that if the id_str is entered the barcodeKit must also be entered
    elif barcode_id_str and not barcodeKit:
        return (
            False,
            validation.required_error(barcodeKit_label, include_error_prefix=True),
            "barcodeKit",
        )  # "Error, Please enter a Barcoding Kit", 'barcodeKit'
    # Next validate that the barcodeKit is spelled correctly
    dnabarcode = models.dnaBarcode.objects.filter(name__iexact=barcodeKit)
    if dnabarcode.count() == 0:
        return (
            False,
            validation.invalid_invalid_value(
                barcodeKit_label, barcodeKit, include_error_prefix=True
            ),
            "barcodeKit",
        )  # "Error, Invalid Barcodekit"
    # Next validate the that id_str is spelled correctly
    dnabarcode = models.dnaBarcode.objects.filter(id_str__iexact=barcode_id_str)
    if dnabarcode.count() == 0:
        return (
            False,
            validation.invalid_invalid_value(
                barcode_id_str_label, barcode_id_str, include_error_prefix=True
            ),
            "barcode_id_str",
        )  # "Error, Invalid barcode"
    # Next validate that the Barcodekit and barcode belong together
    dnabarcode = models.dnaBarcode.objects.filter(
        name__iexact=barcodeKit, id_str__iexact=barcode_id_str
    )
    if dnabarcode.count() != 1:
        return (
            False,
            validation.format(
                ugettext_lazy(
                    "samplesets.samplesetitem.messages.validate.barcodekit.invalid_choice"
                ),
                {
                    "barcode": barcode_id_str,
                    "barcodeSet": barcodeKit,
                    "barcodeSetLabel": barcodeKit_label,
                    "barcodeLabel": barcode_id_str_label,
                },
                include_error_prefix=True,
            ),
            "barcodeKit",
        )

    return True, errorMessage, item

def validate_nucleotideType(nucleotideType, field_label="Sample Nucleotide Type"):
    """
    validate nucleotide type case-insensitively with leading/trailing blanks in the input ignored
    """
    VALID_NUCLEOTIDE_TYPES = [Nuc[0] for Nuc in SampleSetItem.ALLOWED_NUCLEOTIDE_TYPES]
    error = ""
    input = ""

    if nucleotideType:
        input = nucleotideType.strip().lower()
        if input == "fusions":
            input = "rna"

        if not validation.is_valid_choice(input, VALID_NUCLEOTIDE_TYPES):
            return (
                False,
                validation.invalid_choice(
                    field_label, nucleotideType, VALID_NUCLEOTIDE_TYPES
                ),
                input,
            )

    return True, error, input

def validate_sample_data(queryDict, samplesetitem_label=_SampleSetItem.verbose_name):
    """
    validate Sample attributes
    return a list containing error messages
    """
    errors = []

    if not queryDict:
        return (
            False,
            validation.invalid_no_data(samplesetitem_label, include_error_prefix=True),
        )  # "Error, No sample data to validate."

    sampleName = queryDict.get("name", "")
    sampleDisplayedName = queryDict.get("displayedName", "")

    if not sampleName:
        sampleName = sampleDisplayedName.replace(" ", "_")
    if not sampleDisplayedName:
        sampleDisplayedName = sampleName

    isValid, errorMessage = validate_sampleName(sampleName)
    if not isValid:
        errors.append(errorMessage)

    isValid, errorMessage = validate_sampleExternalId(queryDict.get("externalId", ""))
    if not isValid:
        errors.append(errorMessage)

    isValid, errorMessage = validate_sampleDescription(queryDict.get("description", ""))
    if not isValid:
        errors.append(errorMessage)

    isValid, errorMessage = validate_sampleStatus(queryDict.get("status", ""))
    if not isValid:
        errors.append(errorMessage)

    isValid, errorMessage = validate_for_same_sampleName_displayedName(
        sampleDisplayedName, sampleName
    )
    if not isValid:
        errors.append(errorMessage)

    return errors

def validate_for_same_sampleName_displayedName(sampleDisplayedName, sampleName):
    # Verify the sample name and displayed name are similar (allow spaces)
    tmpSampleDisplayedName = "_".join(sampleDisplayedName.split())
    if sampleName != sampleDisplayedName and sampleName != tmpSampleDisplayedName:
        return (
            False,
            "Sample name should match sample displayed name, with spaces allowed in the displayedName",
        )  # TODO: i18n

    return True, None

def validate_sampleStatus(sampleStatus, field_label=_Sample.status.verbose_name):
    allowed_status_list = [s[0] for s in Sample.ALLOWED_STATUS]

    if not validation.is_valid_choice(sampleStatus, allowed_status_list):
        return (
            False,
            validation.invalid_choice(field_label, sampleStatus, allowed_status_list),
        )

    return True, None

def validate_sampleName(sampleName, field_label=_Sample.displayedName.verbose_name):

    isValid, errorMessage = _validate_textValue_mandatory(sampleName, field_label)

    if not isValid:
        return isValid, errorMessage

    isValid, errorMessage = _validate_textValue(sampleName, field_label)
    if not isValid:
        return isValid, errorMessage

    isValid, errorMessage = _validate_textValue_leadingChars(sampleName, field_label)
    if not isValid:
        return isValid, errorMessage

    if not validation.is_valid_length(sampleName.strip(), MAX_LENGTH_SAMPLE_NAME):
        errorMessage = validation.invalid_length_error(
            field_label,
            MAX_LENGTH_SAMPLE_NAME,
            sampleName.strip(),
            include_error_prefix=True,
        )
        return isValid, errorMessage

    return True, None

def validate_pcrPlateRow(pcrPlateRow, field_label="PCR Plate Position"):
    """
    validate PCR plate row case-insensitively with leading/trailing blanks in the input ignored
    """
    isValid = True
    errors = []
    input = ""

    if pcrPlateRow:
        input = pcrPlateRow.strip().upper()
        validValues = [v[0] for v in SampleSetItem.ALLOWED_AMPLISEQ_PCR_PLATE_ROWS_V1]
        if not validation.is_valid_keyword(input, validValues):
            errors.append(validation.invalid_keyword_error(field_label, validValues))
            isValid = False

    return isValid, errors, input

def validate_pcrPlateCol(pcrPlateCol, field_label="PCR Plate Position"):
    """
    validate PCR plate row case-insensitively with leading/trailing blanks in the input ignored
    """
    isValid = True
    errors = []
    input = ""

    if pcrPlateCol:
        input = pcrPlateCol.strip().upper()
        validValues = [
            v[0] for v in SampleSetItem.ALLOWED_AMPLISEQ_PCR_PLATE_COLUMNS_V1
        ]
        if not validation.is_valid_keyword(input, validValues):
            errors.append(validation.invalid_keyword_error(field_label, validValues))
            isValid = False

    return isValid, errors, input

def validate_samplesetStatus(samplesetStatus, field_label="Status"):
    """
    validate samplesetStatus with leading/trailing blanks in the input ignored
    """
    isValid = True
    errors = []
    inputData = ""

    if samplesetStatus:
        inputData = samplesetStatus.strip().lower()
        validValues = [v[0] for v in SampleSet.ALLOWED_SAMPLESET_STATUS]
        if not validation.is_valid_keyword(inputData, validValues):
            errors.append(validation.invalid_keyword_error(field_label, validValues))
            errors = "".join(errors).replace("are ,", ":")
            isValid = False
            return isValid, errors, samplesetStatus

    isValid = True
    return isValid, None, None

def validate_libPrepType(
    libPrepType, field_label="Library Prep Type"
):  # TODO: Dead code?
    """
    validate libPrepType with leading/trailing blanks in the input ignored
    """
    isValid = True
    errors = []
    inputData = ""
    if libPrepType:
        inputData = libPrepType.strip()

        validValues = [v[0] for v in SampleSet.ALLOWED_LIBRARY_PREP_TYPES]
        if not validation.is_valid_keyword(inputData, validValues):
            errors.append(validation.invalid_keyword_error(field_label, validValues))
            errors = "".join(errors).replace("are ,", ":")
            isValid = False
            return isValid, errors, libPrepType

    isValid = True
    return isValid, None, None

def validate_sampleBarcodeMapping(queryDict, sampleSet):
    """
    validate sampleBarcodeMapping input sent via API
        - BarcodeKit, Barcode
        - SampleRow, SampleColumn
    """
    isValid = False
    errordict = {}
    pcrplateBarcodeQueryDict = queryDict.get("sampleBarcodeMapping", None)

    # Input JSON object from Chef to Update
    allBarcodeKits = [
        pcr_plate_barcode["sampleToBarcode"]["barcodeKit"]
        for pcr_plate_barcode in pcrplateBarcodeQueryDict
    ]
    allBarcodes = [
        pcr_plate_barcode["sampleToBarcode"]["barcode"]
        for pcr_plate_barcode in pcrplateBarcodeQueryDict
    ]
    allPcrPlates = [
        pcr_plate_barcode["sampleToBarcode"]["sampleRow"]
        for pcr_plate_barcode in pcrplateBarcodeQueryDict
    ]
    singleBarcode = allBarcodeKits and all(
        allBarcodeKits[0] == elem for elem in allBarcodeKits
    )

    # # validate if same barcode is being used for multiple samples
    if allPcrPlates and len(allBarcodes) != len(set(allBarcodes)):
        dupBarcode = [x for x in allBarcodes if allBarcodes.count(x) >= 2]
        errordict = {
            "result": "1",
            "message": "Fail",
            "detailMessage": validation.format(
                ugettext_lazy(
                    "samplesets.samplesetitem.messages.validate.barcode.unique"
                ),
                {"barcode": ", ".join(dupBarcode)},
                include_error_prefix=True,
            ),  # "Error, A barcode can be assigned to only one sample in the sample set."
            "inputData": dupBarcode,
        }
        return isValid, errordict

    if not singleBarcode:
        errordict = {
            "result": "1",
            "message": "Fail",
            "detailMessage": validation.format(
                ugettext_lazy(
                    "samplesets.samplesetitem.messages.validate.barcodekit.notequal"
                ),
                include_error_prefix=True,
            ),  # "Error, Only one barcode Kit can be used for a sample set"
            "inputData": allBarcodeKits,
        }
        return isValid, errordict

    if pcrplateBarcodeQueryDict:
        sampleSetItems = sampleSet.samples.all()
        userPcrPlates = [item.pcrPlateRow for item in sampleSetItems]
        isValid, errorMessage = validate_user_chef_barcodeKit(
            sampleSetItems, allBarcodeKits, allPcrPlates
        )
        if not isValid:
            return isValid, errorMessage
        for pcr_plate_barcode in pcrplateBarcodeQueryDict:
            barcodeKit = pcr_plate_barcode["sampleToBarcode"]["barcodeKit"]
            barcode = pcr_plate_barcode["sampleToBarcode"]["barcode"]
            row = pcr_plate_barcode["sampleToBarcode"]["sampleRow"]

            # validate pcrPlate Row
            isValid, errormsg, inputData = validate_pcrPlateRow(row)
            if not isValid:
                errordict = {
                    "result": "1",
                    "message": "Fail",
                    "detailMessage": "".join(errormsg),
                    "inputData": inputData,
                }
                return isValid, errordict
            else:
                isValid = False

            # validate pcrPlate Column
            col = pcr_plate_barcode["sampleToBarcode"]["sampleColumn"]
            isValid, errormsg, inputData = validate_pcrPlateCol(col)
            if not isValid:
                errordict = {
                    "result": "1",
                    "message": "Fail",
                    "detailMessage": "".join(errormsg),
                    "inputData": inputData,
                }
                return isValid, errordict
            else:
                isValid = False

            # validate the specified barcode belongs to appropriate barcodeKit
            isValid, errormsg, items = validate_barcodekit_and_id_str(
                barcodeKit, barcode
            )
            if not isValid:
                errordict = {
                    "inputData": [barcodeKit, barcode],
                    "result": "1",
                    "message": "Fail",
                    "detailMessage": errormsg,
                }
                return isValid, errordict

            # Override the barcode and barcodeKit if User specified PCR Plate row and chef specified PCR Plate rows are similar
            # Validate if there is any pcrPlate Row mismatch between User and Chef Inputs for Data integrity
            mistmatch_PcrPlates = set(userPcrPlates) - set(allPcrPlates)
            if len(mistmatch_PcrPlates):
                isValid, errors = validate_barcoding_samplesetitems(
                    sampleSetItems,
                    barcodeKit,
                    barcode,
                    sampleSet.id,
                    allPcrPlates=allPcrPlates,
                    pcrPlateRow=row,
                )
            if not isValid:
                return isValid, errors

    isValid = True
    return isValid, None

def validate_user_chef_barcodeKit(samplesetitems, chef_barcodeKit, allPcrPlates):
    all_TS_barcodeKit = []
    userPcrPlates = []
    isValid = True
    errorMessage = ""

    for item in samplesetitems:
        barcodeKit_TS = item.dnabarcode.name if item.dnabarcode else None
        userPcrPlate = item.pcrPlateRow
        all_TS_barcodeKit.append(barcodeKit_TS)
        userPcrPlates.append(userPcrPlate)
    barcodeKit_mistmatch = set(chef_barcodeKit) - set(all_TS_barcodeKit)
    pcrPlate_mistmatch = set(userPcrPlates) - set(allPcrPlates)

    if len(pcrPlate_mistmatch) and len(barcodeKit_mistmatch):
        print(pcrPlate_mistmatch)
        print(barcodeKit_mistmatch)
        isValid = False
        errorMessage = validation.format(
            ugettext_lazy(
                "samplesets.samplesetitem.messages.validate.barcodekit.notequal"
            ),
            include_error_prefix=True,
        )  # "Error, Only one barcode kit can be used for a sample set"
        return isValid, errorMessage
    return isValid, errorMessage

def validate_sampleSets_for_planning(sampleSets):
    """ Validate multiple sampleSets are compatible to create a Plan from """
    errors = []
    items = SampleSetItem.objects.filter(sampleSet__in=sampleSets)
    if not items:
        errors.append(
            validation.invalid_required_at_least_one_child(
                _SampleSet.verbose_name, _Sample.verbose_name
            )
        )  # 'Sample Set must have at least one sample'
        return errors

    samples_w_barcodes = items.exclude(dnabarcode__isnull=True)
    barcodeKitNames = samples_w_barcodes.values_list(
        "dnabarcode__name", flat=True
    ).distinct()
    if len(barcodeKitNames) > 1:
        errors.append(
            validation.format(
                ugettext_lazy("samplesets.messages.validate.barcodekit.notequal"),
                {"barcodeSetNames": ", ".join(barcodeKitNames)},
            )
        )  # 'Selected Sample Sets have different Barcode Kits: %s.' % ', '.join(barcodeKitNames)
    elif len(barcodeKitNames) == 1:
        barcodes = {}
        for barcode, sample, setname in samples_w_barcodes.values_list(
            "dnabarcode__id_str", "sample__name", "sampleSet__displayedName"
        ):
            if barcode in barcodes:
                msg = validation.format(
                    ugettext_lazy(
                        "samplesets.samplesetitem.messages.validate.barcode.unique.detailed"
                    ),
                    {
                        "barcode": barcode,
                        "sampleName": sample,
                        "sampleSetName": setname,
                        "dup_sampleName": barcodes[barcode][0],
                        "dup_sampleSetName": barcodes[barcode][1],
                    },
                )  # 'Multiple samples are assigned to barcode %s: %s (%s), %s (%s)' % (barcode, sample, setname, barcodes[barcode][0], barcodes[barcode][1])
                errors.append(msg)
            else:
                barcodes[barcode] = (sample, setname)

    return errors

def validate_controlType(controlType, field_label="Control Type"):
    errors, controlType = validate_sampleControlType(controlType, field_label)
    if errors:
        return False, errors[0], ""
    else:
        return True, "", controlType

def validate_libraryPrepKitName(libraryPrepKitName, field_label="Library Prep Kit"):
    errors, _ = validate_optional_kit_name(
        libraryPrepKitName, ["LibraryPrepKit"], field_label
    )
    return errors

def validate_samplesetGroupType(groupType, field_label="Group Type"):
    if groupType:
        groupTypeValues = SampleGroupType_CV.objects.values_list(
            "displayedName", flat=True
        )
        if groupType not in groupTypeValues:
            return (
                False,
                validation.invalid_choice(
                    field_label, groupType, groupTypeValues, include_error_prefix=True
                ),
            )

    return True, None

# Validation check for max samples limit
def validate_sampleset_items_limit(pending_samplesetitems, sampleSet):
    ss_object = SampleSet.objects.get(pk=int(sampleSet[0]))
    # sample set items limit only apply to Ampliseq HD on chef
    if ss_object.libraryPrepType not in VALID_LIB_TYPES:
        return True, None

    available_samples_in_ss = len(ss_object.samples.all())
    pending_samples_to_add = len(pending_samplesetitems)
    total_samples = available_samples_in_ss + pending_samples_to_add
    if sampleSet and total_samples > ASHD_SAMPLESET_ITEM_LIMIT:
        errorMessage = (
            "Error: a Sample Set must contain only %s samples. There are %s specified currently."
            % (ASHD_SAMPLESET_ITEM_LIMIT, total_samples)
        )
        return False, errorMessage

    return True, None

# validation for chef Multi Pool support for OCAv4
def validate_multi_pool_support_samples(all_plates_mapping, sampleSets):
    errors = []
    warning = None

    allSamples = [sampleSet.samples.all() for sampleSet in sampleSets]
    allSampleSetItems = [item for sublist in allSamples for item in sublist]
    if len(allSampleSetItems) > 8:
        warning = "Note: Both Pool1 and Pool2 samples combined are greater than 8 samples."
    allSamplesInOnePlan = allSampleSetItems

    sample_set_config = [sample_set_config.keys() for sample_set_config in all_plates_mapping]
    sampleSetConfigPlates = [item for sublist in sample_set_config for item in sublist]
    if len(sampleSetConfigPlates) != len(set(sampleSetConfigPlates)):
        errors.append("Duplicate plate%s found in combined sampleset"
                      "Samplesets must originate from unique plates to be combined for this application." % [
                          str(plateName) for plateName in
                          sampleSetConfigPlates])
    return allSamplesInOnePlan, errors, warning


import torch
import torch.nn.functional as F
from typeguard import check_argument_types

from espnet2.s2st.losses.abs_loss import AbsS2STLoss
from espnet2.utils.types import str2bool
from espnet.nets.pytorch_backend.e2e_tts_tacotron2 import GuidedAttentionLoss
from espnet.nets.pytorch_backend.nets_utils import to_device

class S2STGuidedAttentionLoss(AbsS2STLoss):
    """Tacotron-based loss for S2ST."""

    def __init__(
        self,
        weight: float = 1.0,
        sigma: float = 0.4,
        alpha: float = 1.0,
    ):
        super().__init__()
        assert check_argument_types()
        self.weight = weight
        self.loss = GuidedAttentionLoss(
            sigma=sigma,
            alpha=alpha,
        )

    def forward(
        self,
        att_ws: torch.Tensor,
        ilens: torch.Tensor,
        olens_in: torch.Tensor,
    ):
        """Forward.

        Args:

        Returns:
            Tensor: guided attention loss
        """
        if self.weight > 0:
            return self.loss(att_ws, ilens, olens_in)
        else:
            return None, None, None, None


import custom_exceptions
import pathlib
import urllib.request

def cleanup_zip_files(updates_dictionary, flag):

    home_path = pathlib.Path('/usr/local/python/wp_updates/wp_updates_dir/')

    for key in updates_dictionary:

        filename = key + '.' + updates_dictionary[key] + '.zip'

        working_file = home_path / 'working' / filename

        if flag == 'p':
            working_file.replace(home_path / 'plugins' / filename)

        elif flag == 't':
            working_file.replace(home_path / 'themes' / filename)

        else:
            raise custom_exceptions.FlagError('Error: Invalid flag. Zip files' +
                ' not cleaned up.')

def check_host(site):

    if site == 'btp':
        url = 'https://btp.umass.edu'
    else:
        url = 'https://' + site + '.chem.umass.edu'

    return (urllib.request.urlopen(url).getcode())


import requests ,json
from bs4 import BeautifulSoup
def numero_cartao():
   bandeira = input('Bandeira do cartão: ')
   url = requests.post('https://www.4devs.com.br/ferramentas_online.php',{'acao' : 'gerar_cc', 'pontuacao' : 'S', 'bandeira' : bandeira}).text
   soup = BeautifulSoup(url, 'html.parser')
   numero_do_cartao = soup.find('div', id="cartao_numero").text.strip()
   data_validade = soup.find('div', id="data_validade").text.strip()
   cvv = soup.find('div', id="codigo_seguranca").text.strip()
   cartao = {'Numero do Cartao' : numero_do_cartao, 'Data de Validade' : data_validade, 'Codico de Seguranca(CVV)' : cvv}
   with open('api_cartao-fail.json', 'w', encoding='UTF-8') as json_cartao:
      json.dump(cartao, json_cartao, ensure_ascii=False, indent=3)
def conta_bancaria():
   estado = input('Estado: ')
   url = requests.post('https://www.4devs.com.br/ferramentas_online.php',{'acao' : 'gerar_conta_bancaria', 'estado' : estado, 'banco' : ''}).text
   soup = BeautifulSoup(url, 'html.parser')
   conta = soup.find('div', id="conta_corrente").text
   agencia = soup.find("div", id="agencia").text.strip()
   banco = soup.find("div", id="banco").text.strip()
   cidade = soup.find("div", id="cidade").text.strip()
   estado = soup.find("div", id="estado").text.strip()
   gerar = {'Conta' : conta, 'Agencia' : agencia, 'Banco' : banco, 'Cidade' : cidade, 'Estado' : estado}
   with open('api_conta-fail.json', 'w', encoding='UTF-8') as json_file:
      json.dump(gerar, json_file, ensure_ascii=False, indent=5)
def gerador_de_cpf():
   uf = input('Uf do estado: ')
   quant = int(input('Quantidade de cpf: '))
   for i in range(0,quant):
        url = requests.post('https://www.4devs.com.br/ferramentas_online.php',{'acao' : 'gerar_cpf',  'pontuacao' : 'S', 'cpf_estado' : uf}).text
        print(url)
def gerador_de_cnpj():
   quantidade = int(input('Quantidade de CNPJ: '))
   for cnp in range(0, quantidade):
        url_2 = requests.post('https://www.4devs.com.br/ferramentas_online.php',{'acao' : 'gerar_cnpj', 'pontuacao' : 'S'}).text
        print(url_2)


from models import URLtoTaskID
from djcelery.models import TaskMeta
from django.template.loader import render_to_string

"""
NOTES:
    url entries should be unique -- a single url maps to a single task_id in a one-to-one fashion
    this strategy allows the asynchronous models to serve as a cache for those apps that wish to use it as such

    check_status_or_begin does not check to see if process is already complete
    if the caller wants the process to be run ONLY IF it hasn't been completed already,
    the caller should check process_is_complete instead

    process_is_running (called on its own or from check_status_or_begin or process_is_running_or_complete) checks
    only if status == 'STARTED', this seems like the safest strategy although it does have it's shortcomings.
    When a task is triggered, it is stored in the celery_taskmeta table with status == PENDING, as soon as
    a worker is assigned to that task, the status becomes STARTED, when the task is complete, status becomes SUCCESS.
    If something goes wrong during the STARTED phase (celeryd is shut down, etc), then status becomes FAILURE.
    If celeryd is not communicating with ghettoq, or not running at all, then the status will remain as PENDING.
    If we assume that PENDING means STARTED, then we may be mislead into thinking a task is being processed,
    when in fact it is sitting idle.  This may only become apparent after waiting way-too-long for the task
    to complete.
    If we only check for STARTED, then each time the task is requested, we will add a new task to the queue.  This
    may back up the queue, but only in situations when there is something wrong to begin with.  This will also
    likely alert the user (or the developer) more immediately that something has gone wrong with celeryd (or ghettoq,
    or whatever) since the returned statement will be something like 'the process has begun', even with subsequent
    requests which should have changed the statement to something like 'the process is already running'.  There is
    also the slight chance that a task will be requested in such quick succession that a worker will not yet be assigned
    to that task and the status will still be PENDING, in which case a new task will be added to the queue.  This seems
    a small price to pay for the more immediate recognition that there is indeed a problem.  As the developer in this
    case, I'd rather get the impression of a False Negative (thinking there may be a problem, when in fact there is none),
    than a False Positive (given the impression that everything is fine, when in fact there is a problem).  Also, the
    user is more likely to notice a problem sooner in the case when there is a hint in the returned statement (that
    the process has begun...yet again), than in the case when they must figure out there is a problem only because
    the process is taking an inordinately long time (in many cases this could mean hours rather than minutes).
"""

'''
begin_process starts the background process
if the process has already been run, the related URLtoTaskID entry is cleared and re-calculated
if the process is currently running, the related URLtoTaskID entry is cleared, and the process is started again
(in this case more than one identical process may be running at the same time -- to prevent this see NOTES above)
called by check_status_or_begin
'''
def __begin_process(task_method, task_args=(), task_kwargs={}, polling_url=None, cache_results=True):
    #see if task exists already
    try:
        URLtoTaskID.objects.get(url=polling_url).delete()
    except:
        pass
    #initialize task
    task = task_method.delay(*task_args, **task_kwargs)
    if polling_url and cache_results:
        URLtoTaskID(url=polling_url, task_id=task.task_id).save()
    return task.task_id

'''
check_status_or_begin
if the user has supplied neither the polling_url nor the task_id, it is assumed that they wish to start the process
(since no process_is_running check can be made without a process identifier anyway)
otherwise, if check_running is left to its default value of True, then a check will be made to see if the process is
already running, in which case a related message is returned to the user and the process is not re-started
if the process is not already running (or the user doesn't wish to check for that case), then the process will be
added to the queue, and a message indicating the process has begun will be returned to the user
if the process had already completed (status == SUCCESS rather than STARTED), it will be run again
if the caller wants the process to be run ONLY IF it hasn't been completed already,
the caller should check process_is_complete instead
'''
def check_status_or_begin(task_method, task_args=(), task_kwargs={}, polling_url=None, task_id=None, check_running=True, cache_results=True):
    if polling_url is None and task_id is None:
        check_running = False
    if check_running and process_is_running(polling_url, task_id):
        if task_id is None:
            task_id = get_taskid_from_url(polling_url)
        return render_to_string('already_processing.html', {}), task_id
    else:
        task_id = __begin_process(task_method, task_args, task_kwargs, polling_url)
        return render_to_string('starting_process.html', {}), task_id

#returns boolean based on process.status == 'STARTED' or 'SUCCESS' (currently running or complete)
def process_is_running_or_complete(polling_url=None, task_id=None):
    if process_is_running(polling_url, task_id) or process_is_complete(polling_url, task_id):
        return True
    else:
        return False

#returns boolean based on whether process is in cache and marked as STARTED
def process_is_running(polling_url=None, task_id=None):
    result = __get_asyncresult(polling_url, task_id)
    if result is not None and result.status == 'STARTED':
        return True
    else:
        return False

#returns boolean based on whether process is in cache and marked as PENDING
# i.e. it has been sent to the queue but processing has not yet begun
def process_is_pending(polling_url=None, task_id=None):
    result = __get_asyncresult(polling_url, task_id)
    if result is not None and result.status == 'PENDING':
        return True
    else:
        return False

#returns boolean value based on result=='SUCCESS' from celery table
def process_is_complete(polling_url=None, task_id=None):
    result = __get_asyncresult(polling_url, task_id)
    if result is not None and result.status == 'SUCCESS':
        return True
    else:
        return False

#returns status string
def get_process_status(polling_url=None, task_id=None):
    result = __get_asyncresult(polling_url, task_id)
    if result is not None:
        return result.status
    else:
        return None

#returns result.result from celery table
def get_process_result(polling_url=None, task_id=None):
    result = __get_asyncresult(polling_url, task_id)
    if result is not None:
        return result.result
    else:
        return None

#get task_id from URLtoTaskID table
def get_taskid_from_url(polling_url):
    try:
        entry = URLtoTaskID.objects.get(url=polling_url)
        return entry.task_id
    except:
        #raise ValueError("Given URL does not map to any known task_id")
        return None

#get url from URLtoTaskID table
def get_url_from_taskid(task_id):
    try:
        entry = URLtoTaskID.objects.get(task_id=task_id)
    except:
        raise ValueError("Given task_id does not map to any known URL")
    return entry.url

#get the AsyncResult object associated with the given (directly or indirectly) task_id
#(this object provides us access to the status field)
def __get_asyncresult(polling_url=None, task_id=None):
    if polling_url == task_id == None:
        raise ValueError("Either polling_url or task_id must be passed a value")
    if task_id is None:
        task_id = get_taskid_from_url(polling_url)
    from celery import result
    result = result.AsyncResult(task_id)
    if result.task_id == None:
        return None
    return result


import argparse
import json
import logging
import os
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta
from tempfile import TemporaryDirectory
from typing import List
from urllib.parse import urlparse, ParseResult, urlunparse

import boto3
import dask
import numpy as np
import pandas as pd
import xarray as xr
import zarr
from aiobotocore.session import AioSession as S3FSSession
from botocore.exceptions import ClientError
from dask.diagnostics import ProgressBar
from s3fs import S3Map, S3FileSystem

XARRAY_8016 = tuple([int(n) for n in xr.__version__.split('.')[:3]]) >= (2023, 8, 0)
TIME_CHUNKING = (4000,)
ISO_8601_D = "%Y-%m-%d"
ISO_8601_DT = "%Y-%m-%dT%H:%M:%S%zZ"

NP_EPOCH = np.datetime64('1970-01-01T00:00:00')

MAX_CONCURRENCY = min(max(1, int(os.getenv('MAX_WORKERS', 12))), 12)

logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] [%(name)s::%(lineno)d] %(message)s')
logger = logging.getLogger('climatology.main')

VERSION = '1.0.0'

session = None

def get_boto_session(args=None):
    global session

    if session is not None:
        return session

    if args is None:
        return None

    logger.info('Creating global boto session')

    try:
        kwargs = dict(region_name=args.aws_region)

        if args.aws_profile:
            kwargs['profile_name'] = args.aws_profile

        session = boto3.session.Session(**kwargs)
        return session
    except:
        return None

def exists(url: ParseResult, args):
    logger.info(f'Checking if {urlunparse(url)} exists')

    if url.scheme in ['', 'file']:
        return os.path.exists(url.path)
    elif url.scheme == 's3':
        client = get_boto_session(args).client(
            's3',
            aws_access_key_id=args.aws_key,
            aws_secret_access_key=args.aws_secret,
            region_name=args.aws_region
        )

        bucket = url.netloc
        key = url.path

        # key will likely be of the form '/path/to/root'. Remove leading / and append /.zgroup
        # since head_object doesn't seem to work for directories. This will also ensure that we
        # return true iff a zarr array exists @ this path
        if key[0] == '/':
            key = key[1:]

        if key[-1] == '/':
            key = f'{key}.zgroup'
        else:
            key = f'{key}/.zgroup'

        try:
            client.head_object(Bucket=bucket, Key=key)
            return True
        except client.exceptions.NoSuchKey:
            return False
        except ClientError as e:
            r = e.response
            err_code = r["Error"]["Code"]

            if err_code == '404':
                return False

            logger.error(f'An AWS error occurred: Code={err_code} Message={r["Error"]["Message"]}')
            raise
        except Exception as e:
            logger.error('Something went wrong!')
            logger.exception(e)
            raise
    else:
        raise ValueError(f'Invalid URL type: {url.scheme}')

def open_dataset(url: ParseResult, args):
    logger.info(f'Opening dataset at {urlunparse(url)}')

    if url.scheme in ['', 'file']:
        store = url.path
    else:
        if args.aws_profile:
            session = S3FSSession(profile=args.aws_profile)
            s3 = S3FileSystem(
                False,
                session=session,
                client_kwargs=dict(region_name=args.aws_region)
            )
        else:
            s3 = S3FileSystem(
                False,
                key=args.aws_key,
                secret=args.aws_secret,
                client_kwargs=dict(region_name=args.aws_region)
            )

        store = S3Map(root=urlunparse(url), s3=s3, check=False)

    return xr.open_zarr(store, mask_and_scale=True)

def main():
    args = parse_args()

    input_url = urlparse(args.input)
    output_url = urlparse(args.output)

    if not exists(input_url, args):
        raise FileNotFoundError(f'No zarr data found at {urlunparse(input_url)}')

    if not args.overwrite and exists(output_url, args):
        raise ValueError(f'Something exists already at {urlunparse(output_url)}')

    logger.info('Opening input dataset')

    input_ds = open_dataset(input_url, args)

    orig_chunk = input_ds.xco2.data.chunksize

    start_date = datetime.utcfromtimestamp(
        (input_ds.time[0].to_numpy() - NP_EPOCH) / np.timedelta64(1, 's')
    )

    end_date = datetime.utcfromtimestamp(
        (input_ds.time[-1].to_numpy() - NP_EPOCH) / np.timedelta64(1, 's')
    )

    if args.start and args.start > start_date:
        start_date = args.start

    if args.end and args.end < end_date:
        end_date = args.end

    logger.info(f'Determining date ranges between {start_date.strftime(ISO_8601_D)} and {end_date.strftime(ISO_8601_D)}')

    freq = 'M' if args.span in ['monthly', 'seasonal', 'monthly-consolidated'] else 'A'

    pd_dates_i: pd.DatetimeIndex = pd.date_range(
        start=start_date,
        end=end_date,
        freq=freq,
    )

    pd_dates: List[datetime] = [d.to_pydatetime().replace(hour=start_date.hour) for d in pd_dates_i]

    if args.span == 'seasonal':
        pd_dates = [dt.replace(day=20) for dt in list(filter(lambda d: d.month % 3 == 0, pd_dates))]

    time_slices = [
        slice(start_date, pd_dates[0])
    ]

    for i in range(1, len(pd_dates)):
        time_slices.append(
            slice(
                pd_dates[i-1] + timedelta(days=1),
                pd_dates[i]
            )
        )

    time_slices.append(slice(pd_dates[-1] + timedelta(days=1), end_date))

    logger.info(f'Will process {len(time_slices)} date ranges')

    slice_datasets = []

    with TemporaryDirectory(prefix='clim-', ignore_cleanup_errors=True) as td:
        for s in time_slices:
            sd = s.start.strftime(ISO_8601_D)

            subset = input_ds.sel(time=s)
            subset = subset.drop_vars(['target_id', 'target_type'], errors='ignore')

            xco2 = subset.xco2.mean(dim='time', skipna=True, keep_attrs=True).expand_dims()
            xco2_uncertainty = subset.xco2_uncertainty.mean(dim='time', skipna=True, keep_attrs=True)
            count: xr.DataArray = subset.xco2.count(dim='time')
            count = count.astype('int')

            slice_time = np.array([s.start], dtype='<M8[ns]')

            slice_ds = xr.Dataset(
                data_vars=dict(
                    xco2=xco2,
                    xco2_uncertainty=xco2_uncertainty,
                    valid_count=count
                ),
                coords=dict(
                    longitude=subset.coords['longitude'],
                    latitude=subset.coords['latitude'],
                )
            )

            slice_ds = slice_ds.expand_dims({"time": 1}).assign_coords({"time": slice_time})

            n_days = xr.DataArray(
                data=np.array([len(subset.time)]),
                dims=['time'],
            ).assign_coords({"time": slice_time})

            slice_ds['n_days'] = n_days

            compressor = zarr.Blosc(cname='blosclz', clevel=9)

            encoding = {var: {'compressor': compressor} for var in slice_ds.data_vars}
            encoding['valid_count']['_FillValue'] = 0

            slice_ds['time'] = slice_ds['time'].chunk(TIME_CHUNKING)

            if XARRAY_8016:
                kwargs = dict(write_empty_chunks=False)
            else:
                kwargs = {}

                for var in encoding:
                    encoding[var]['write_empty_chunks'] = False

            tmp_path = os.path.join(td, f'{sd}.zarr')

            logger.info(f'Writing slice to temp Zarr group at {tmp_path}')

            with ProgressBar():
                slice_ds.to_zarr(
                    tmp_path,
                    mode='w',
                    encoding=encoding,
                    consolidated=True,
                    **kwargs
                )

            slice_datasets.append(xr.open_zarr(tmp_path))

        if args.span == 'monthly-consolidated':
            logger.info('Consolidating months')

            consolidated_slices = []
            months = {}

            for i in range(1, 13):
                months[i] = []

            for s in slice_datasets:
                slice_time = datetime.utcfromtimestamp(
                    (s.time[0].to_numpy() - NP_EPOCH) / np.timedelta64(1, 's')
                )

                months[slice_time.month].append(s)

            for month in months:
                slices = months[month]

                if len(slices) == 0:
                    continue

                dt = datetime(1970, month, 1, 0, 0, 0)
                dt = np.array([dt], dtype='<M8[ns]')

                slices = xr.concat(slices, dim='time')

                xco2 = slices.xco2.mean(dim='time', skipna=True, keep_attrs=True).expand_dims()
                xco2_uncertainty = slices.xco2_uncertainty.mean(dim='time', skipna=True, keep_attrs=True)
                count = slices.valid_count.sum(dim='time').astype('int')
                n_days = slices.n_days.sum(dim='time')

                month_ds = xr.Dataset(
                    data_vars=dict(
                        xco2=xco2,
                        xco2_uncertainty=xco2_uncertainty,
                        valid_count=count,
                    ),
                    coords=dict(
                        longitude=slices.coords['longitude'],
                        latitude=slices.coords['latitude'],
                    )
                ).expand_dims({"time": 1}).assign_coords({"time": dt})

                month_ds['n_days'] = n_days

                consolidated_slices.append(month_ds)

            slice_datasets = consolidated_slices

        clim_ds = xr.concat(slice_datasets, dim='time').sortby('time')

        clim_ds.attrs.update(input_ds.attrs)

        if args.span == 'annual':
            comment_avg_string = 'annually'
        elif args.span == 'seasonal':
            comment_avg_string = 'seasonally'
        elif args.span == 'monthly':
            comment_avg_string = 'monthly'
        else:
            comment_avg_string = 'monthly and consolidated to a single year'

        clim_ds.attrs.update(dict(
            coverage_start=start_date.strftime(ISO_8601_DT),
            coverage_end=end_date.strftime(ISO_8601_DT),
            date_created=datetime.utcnow().strftime(ISO_8601_DT),
            source=f'Produced from averaging the {input_ds.attrs["title"]} dataset, itself '
                   f'd{input_ds.attrs["comment"][1:]}',
            title=f'{input_ds.attrs["title"]}_clim',
            comment=f'Gridded CO2 column-average data averaged {comment_avg_string}',
            averaging_span=args.span,
            source_pipeline_version=input_ds.attrs['pipeline_version'],
            climatology_tool_version=VERSION
        ))

        del clim_ds.attrs['date_updated']
        del clim_ds.attrs['pipeline_version']

        clim_ds.valid_count.attrs.update(dict(
            long_name='valid_days_per_pixel',
            comment='Number of valid (non-NaN) days that was used to compute the means of each pixel of the xco2 and '
                    'xco2_uncertainty variables',
            units='number of days'
        ))

        clim_ds.n_days.attrs.update(dict(
            long_name='number_of_source_days',
            comment='Total number of source days per span of climatology data',
            units='number of days'
        ))

        output_ext = args.output.split('.')[-1].rstrip('/')

        if output_ext == 'zarr':
            if output_url.scheme in ['', 'file']:
                store = output_url.path
            else:
                if args.aws_profile:
                    session = S3FSSession(profile=args.aws_profile)
                    s3 = S3FileSystem(
                        False,
                        session=session,
                        client_kwargs=dict(region_name=args.aws_region)
                    )
                else:
                    s3 = S3FileSystem(
                        False,
                        key=args.aws_key,
                        secret=args.aws_secret,
                        client_kwargs=dict(region_name=args.aws_region)
                    )

                store = S3Map(root=args.output, s3=s3, check=False)

            if args.rechunk:
                output_chunk = args.rechunk
            else:
                output_chunk = orig_chunk

            logger.info(f'Setting output chunks {output_chunk}')

            for var in clim_ds.data_vars:
                clim_ds[var] = clim_ds[var].chunk(output_chunk)

            compressor = zarr.Blosc(cname='blosclz', clevel=9)

            encoding = {var: {'compressor': compressor} for var in clim_ds.data_vars}
            encoding['valid_count']['_FillValue'] = 0

            clim_ds['time'] = clim_ds['time'].chunk(TIME_CHUNKING)

            if XARRAY_8016:
                kwargs = dict(write_empty_chunks=False)
            else:
                kwargs = {}

            logger.info(f'Writing Zarr array to {args.output}')

            with ProgressBar():
                clim_ds.to_zarr(
                    store,
                    mode='w',
                    encoding=encoding,
                    consolidated=True,
                    **kwargs
                )

        elif output_ext in ['nc', 'nc4']:
            comp = {"zlib": True, "complevel": 9}
            encoding = {var: comp for var in clim_ds.data_vars}
            encoding['valid_count']['_FillValue'] = 0

            if output_url.scheme in ['', 'file']:
                logger.info(f'Writing NetCDF to {args.output}')

                with ProgressBar():
                    clim_ds.to_netcdf(
                        output_url.path,
                        mode='w',
                        encoding=encoding
                    )
            else:
                with TemporaryDirectory(prefix='clim-s3-', ignore_cleanup_errors=True) as td:
                    filename = os.path.basename(args.output)
                    temp_file = os.path.join(td, filename)

                    logger.info(f'Writing NetCDF to temp file at {temp_file}')

                    with ProgressBar():
                        clim_ds.to_netcdf(
                            temp_file,
                            mode='w',
                            encoding=encoding
                        )

                    logger.info('Uploading file to S3')

                    client = get_boto_session(args).client(
                        's3',
                        aws_access_key_id=args.aws_key,
                        aws_secret_access_key=args.aws_secret,
                        region_name=args.aws_region
                    )

                    bucket = output_url.netloc
                    key = output_url.path

                    # key will likely be of the form '/path/to/root'. Remove leading / and append /.zgroup
                    # since head_object doesn't seem to work for directories. This will also ensure that we
                    # return true iff a zarr array exists @ this path
                    if key[0] == '/':
                        key = key[1:]

                    try:
                        client.upload_file(temp_file, bucket, key)
                    except ClientError as e:
                        r = e.response
                        err_code = r['Error']['Code']

                        logger.error(f'An AWS error occurred: Code={err_code}; Message={r["Error"]["Message"]}')
                        logger.debug(f'Full error message: \n {json.dumps(r, indent=4)}')

                        raise OSError('Failed to upload output file')
        else:
            raise ValueError(f'Unsupported output file type: .{output_ext}')

def parse_args():
    parser = argparse.ArgumentParser()

    def parse_iso(s):
        return datetime.strptime(s, ISO_8601_D)

    parser.add_argument(
        '-i', '--input',
        required=True,
        metavar='URL',
        help='Path or URL to input Zarr dataset. Must either be in S3 or local FS',
        dest='input'
    )

    parser.add_argument(
        '-o', '--output',
        required=True,
        metavar='URL',
        help='Path or URL to output Zarr dataset. Must either be in S3 or local FS. Raises an error if something exists'
             ' here. Use option --overwrite to go around this.',
        dest='output'
    )

    parser.add_argument(
        '--overwrite',
        required=False,
        action='store_true',
        help='Overwrite existing data if something exists at output path',
        dest='overwrite'
    )

    parser.add_argument(
        '-s', '--start-date',
        required=False,
        help='Start of date range from which climatology will be built, if not given or if given date is before the '
             'earliest date of input data, the dataset will begin building from its start date. Arg should be ISO '
             'formatted string',
        dest='start',
        type=parse_iso
    )

    parser.add_argument(
        '-e', '--end-date',
        required=False,
        help='End of date range to which climatology will be built, if not given or if given date is after the '
             'latest date of input data, the dataset will be built to its end dateArg should be ISO '
             'formatted string',
        dest='end',
        type=parse_iso
    )

    parser.add_argument(
        '--span',
        required=False,
        default='monthly',
        choices=['monthly', 'seasonal', 'annual', 'monthly-consolidated'],
        help='The span of time to average out for each step of climatology. \'monthly-consolidated\' will consolidate '
             'each month\'s data and map them to 1970-mm-01.',
        dest='span'
    )

    parser.add_argument(
        '--rechunk',
        nargs=3,
        required=False,
        dest='rechunk',
        type=int,
        help='Chunk shape for output dataset in order: time longitude latitude',
        metavar=('TIME', 'LON', 'LAT')
    )

    aws = parser.add_argument_group('AWS options')

    aws.add_argument(
        '--key',
        required=False,
        dest='aws_key',
        help='AWS Access Key ID',
    )

    aws.add_argument(
        '--secret',
        required=False,
        dest='aws_secret',
        help='AWS Secret Access Key',
    )

    aws.add_argument(
        '--profile',
        required=False,
        dest='aws_profile',
        help='AWS profile',
    )

    aws.add_argument(
        '--region',
        required=False,
        dest='aws_region',
        help='AWS region',
        default='us-west-2'
    )

    args = parser.parse_args()

    if args.rechunk:
        args.rechunk = tuple(args.rechunk)

    if args.start and args.end:
        if args.start == args.end:
            raise ValueError('Start and end times must be different')
        if args.end < args.start:
            raise ValueError('End time parameter must not be before start time')

    aws = urlparse(args.input).scheme == 's3' or urlparse(args.output).scheme == 's3'

    if aws and not ((args.aws_key and args.aws_secret) or args.aws_profile):
        raise ValueError('AWS credentials MUST be provided to read to or write from S3')

    return args

if __name__ == '__main__':
    logger.info(f'Using max concurrency: {MAX_CONCURRENCY}')

    start_time = datetime.now()

    dask.config.set(
        pool=ThreadPoolExecutor(
            max_workers=MAX_CONCURRENCY,
            thread_name_prefix='dask-worker'
        )
    )

    try:
        main()
        logger.info('Script completed successfully')
    except Exception as e:
        logger.exception(e)
        logger.info('Script completed unsuccessfully')
    except KeyboardInterrupt:
        logger.info('Script was cancelled before it could finish')
    finally:
        logger.info(f'Script exited after {datetime.now() - start_time}')


#Bernardo Carlos Franceschina - 20203080
#Enzo Bassani - 20200398
from helpers.BernardoEnzo_helper import *
from models.BernardoEnzo_Agenda import Agenda
from models.BernardoEnzo_ListaClientes import ListaClientes
from models.BernardoEnzo_ListaProcedimentos import ListaProcedimentos
from models.BernardoEnzo_Pet import *
from datetime import date

clientes = ListaClientes()
agenda = Agenda()
procedimentos = ListaProcedimentos()

def main():
    while True:
        option = input("Selecione uma opção:\n1) Clientes\n2) Agenda\n3) Procedimentos\n4) Caixa\n0) Encerrar o programa\n")
        print()
        if option == "1":
            menuClientes()
        elif option == "2":
            menuAgenda()
        elif option == "3":
            menuProcedimentos()
        elif option == "4":
            menuCaixa()
        elif option == "0":
            return
        else:
            print("Comando inválido")

def menuAgenda():
    while True:
        option = input("\nSelecione uma opção:\n1) Cadastrar novo evento\n2) Editar Evento\n3) Excluir evento\n4) Exibir agenda do dia\n5) Exibir agenda de amanhã\n6) Exibir um período específico\n7) Exibir todos os eventos\n0) Voltar\n")
        if option == "1":
            agenda.novoEvento(clientes, procedimentos)
        elif option == "2":
            eventoIndex = agenda.selecionarEvento()
            if eventoIndex == -1:
                print("Procedimento não encontrado")
            else:
                agenda.agenda[eventoIndex].editarEvento(clientes, procedimentos, agenda, eventoIndex)
        elif option == "3":
            eventoIndex = agenda.selecionarEvento()
            if eventoIndex == -1:
                print("Procedimento não encontrado")
            else:
                #agenda.agenda[eventoIndex].excluirEvento()
                agenda.agenda.pop(eventoIndex)
        elif option == "4":
            hoje = date.today()
            agenda.printAgenda(hoje, hoje)
        elif option == "5":
            hoje = date.today()
            hoje.replace(day = hoje.day + 1)
            agenda.printAgenda(hoje, hoje)
        elif option == "6":
            inicio = getData("Desde (dd/mm/aa): ", False)
            if inicio == -1:
                continue
            fim = getData("Até (dd/mm/aa): ", False)
            if fim == -1:
                continue
            agenda.printAgenda(inicio, fim)
        elif option == "7":
            agenda.printTodosEventos()
        elif option == "0":
            return
        else:
            print("Comando inválido")

def menuProcedimentos():
    while True:
        option = input("\nSelecione uma opção:\n1) Cadastrar procedimento\n2) Editar procedimento\n3) Excluir procedimento\n4) Exibir todos os procedimentos\n0) Voltar\n")
        if option == "1":
            procedimentos.novoProcedimento()
        elif option == "2":
            procedimento = procedimentos.encontrarProcedimento(input("Nome: "))
            if procedimento == -1:
                print("Procedimento não encontrado")
            else: procedimentos.procedimentos[procedimento].editar()
        elif option == "3":
            procedimento = procedimentos.encontrarProcedimento(input("Nome: "))
            if procedimento == -1:
                print("Procedimento não encontrado")
            else: procedimentos.excluirProcedimento(procedimento)
        elif option == "4":
            procedimentos.listarProcedimentos()
        elif option == "0":
            return
        else: print("Comando inválido")

def menuClientes():
    while True:
        option = input("Selecione uma opção:\n1) Cadastrar cliente\n2) Selecionar cliente\n3) Exibir todos os cadastros\n0) Voltar\n")
        if option == "1":
            clientes.novoCliente()
        elif option == "2":
            selecionarCliente()
        elif option == "3":
            clientes.printLista(True, True)
        elif option == "0":
            return
        else:
            print("Comando inválido")

def selecionarCliente():

    clienteIndex = clientes.encontrarCliente(input("Nome/CPF do Cliente: "))
    if clienteIndex == -1:
        print("Cliente não encontrado.")
        if getAnswer("Deseja ver a lista de clientes e seus animais?(S/N) ") == 'S':
            clientes.printLista(False, True)
        return
    if clienteIndex == -2:
        return

    print("\nCliente selecionado:", clientes.clientes[clienteIndex].nome)
    while True:
        option = input(
            "Selecione uma opção:\n1) Novo animal\n2) Editar cadastro\n3) Excluir cadastro\n4) Exibir informações\n0) Voltar ao menu principal\n\n")
        if option == "1":
            clientes.clientes[clienteIndex].novoPet(clientes)
        elif option == "2":
            clientes.clientes[clienteIndex].editarCliente()
        elif option == "3":
            while True:
                optionExcluir = input('Selecione uma opção:\n1) Excluir Cliente\n2) Excluir Animal\n0) Voltar\n')
                if optionExcluir == "1":
                    if getAnswer(f'Deseja excluir este cliente?') == "S":
                        del (clientes.clientes[clienteIndex])
                        return
                elif optionExcluir == "2":
                    clientes.clientes[clienteIndex].printData(False, True)
                    while True:
                        print("Digite 0 para voltar")
                        nomePet = input("Nome: ").capitalize()
                        if nomePet == "0":
                            break
                        petIndex = -1
                        for pet in range(len(clientes.clientes[clienteIndex].pets)):
                            if clientes.clientes[clienteIndex].pets[pet].nome == nomePet:
                                petIndex = pet
                        if petIndex == -1:
                            print("Pet não encontrado")
                            continue
                        else:
                            if getAnswer(f'Deseja excluir este animal? (S/N)') == "S":
                                if isinstance(clientes.clientes[clienteIndex].pets[petIndex], Vip):
                                    clientes.vips -= 1
                                clientes.clientes[clienteIndex].pets.pop(petIndex)
                                break

                elif optionExcluir == "0":
                        break
                else: print("Comando inválido")

        elif option == "4":
            clientes.clientes[clienteIndex].printData(True, True)
        elif option == "0":
            return
        else:
            print("Comando inválido")

def menuCaixa():
    while True:
        option = input("\nSelecione uma opção:\n1) Ver fluxo de caixa esperado para o mês atual\n2) Ver fluxo de caixa esperado para este e para os próximos meses\n3) Ver fluxo de caixa dos últimos meses\n4) Ver fluxo de caixa de um período personalizado\n0) Voltar\n")
        if option == "1":
            inicio = date.today()
            fim = inicio
            inicio = inicio.replace(day = 1)
            try:
                fim = fim.replace(day = 31)
            except:
                fim = fim.replace(day = 30)
            print("Fluxo de caixa do mês atual:", agenda.getCaixa(inicio, fim, clientes))
        elif option == "2":
            meses = getnum("Deseja calcular o caixa de quantos meses além do atual? ", 1, 1, 12000)
            inicio = date.today()
            fim = inicio
            inicio = inicio.replace(day = 1)
            fim = fim.replace(year = fim.year + int((fim.month + meses)/12))
            fim = fim.replace(month = (fim.month + meses - 1)%12 + 1)
            try:
                fim = fim.replace(day = 31)
            except:
                fim = fim.replace(day = 30)
            print("Fluxo de caixa do(s)", meses, "próximo(s) mês(es):", agenda.getCaixa(inicio, fim, clientes))

        elif option == "3":
            meses = getnum("Deseja calcular o caixa de quantos dos meses anteriores?", 1, 1, 12000)
            inicio = date.today()
            fim = inicio
            inicio = inicio.replace(day = 1)
            inicio = inicio.replace(year = inicio.year - int((12 - inicio.month + meses)/12))
            inicio = inicio.replace(month = (inicio.month - meses - 1)%12 + 1)
            if fim.month == 1:
                fim = fim.replace(month = 12)
                fim = fim.replace(year = fim.year - 1)
            else: fim = fim.replace(month = fim.month - 1)
            try:
                fim = fim.replace(day = 31)
            except:
                fim = fim.replace(day = 30)
            print("Fluxo de caixa do(s)", meses, "mês(es) anterior(es):", agenda.getCaixa(inicio, fim, clientes))
        elif option == "4":
            inicio = getData("Desde (dd/mm/aa): ", False)
            if inicio == -1:
                continue
            fim = getData("Até (dd/mm/aa): ", False)
            if fim == -1:
                continue
            print("Fluxo de caixa do período:", agenda.getCaixa(inicio, fim, clientes))

        elif option == "0":
            return
        else: print("Comando inválido")
main()