import copy
from datetime import datetime, timedelta
from dateutil.tz import tzutc
from flask import current_app as app, g
from marshmallow import Schema as MarshmallowSchema, ValidationError, fields as f, validates_schema, pre_load, post_load
from marshmallow.fields import Boolean, DateTime, Decimal, Float, Integer, Nested, String, \
    TimeDelta, UUID
from marshmallow.validate import Length, OneOf, Range
from sqlalchemy.util import OrderedSet
from teal.enums import Country, Currency, Subdivision
from teal.marshmallow import EnumField, IP, SanitizedStr, URL, Version
from teal.resource import Schema

from ereuse_devicehub.marshmallow import NestedOn
from ereuse_devicehub.resources import enums
from ereuse_devicehub.resources.action import models as m
from ereuse_devicehub.resources.agent import schemas as s_agent
from ereuse_devicehub.resources.device import schemas as s_device
from ereuse_devicehub.resources.tradedocument import schemas as s_document
from ereuse_devicehub.resources.documents import schemas as s_generic_document
from ereuse_devicehub.resources.enums import AppearanceRange, BiosAccessRange, FunctionalityRange, \
    PhysicalErasureMethod, R_POSITIVE, RatingRange, \
    Severity, SnapshotSoftware, TestDataStorageLength
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
from ereuse_devicehub.resources.schemas import Thing
from ereuse_devicehub.resources.user import schemas as s_user
from ereuse_devicehub.resources.user.models import User
from ereuse_devicehub.resources.tradedocument.models import TradeDocument


class Action(Thing):
    __doc__ = m.Action.__doc__
    id = UUID(dump_only=True)
    name = SanitizedStr(default='',
                        validate=Length(max=STR_BIG_SIZE),
                        description=m.Action.name.comment)
    closed = Boolean(missing=True, description=m.Action.closed.comment)
    severity = EnumField(Severity, description=m.Action.severity.comment)
    description = SanitizedStr(default='', description=m.Action.description.comment)
    start_time = DateTime(data_key='startTime', description=m.Action.start_time.comment)
    end_time = DateTime(data_key='endTime', description=m.Action.end_time.comment)
    snapshot = NestedOn('Snapshot', dump_only=True)
    agent = NestedOn(s_agent.Agent, description=m.Action.agent_id.comment)
    author = NestedOn(s_user.User, dump_only=True, exclude=('token',))
    components = NestedOn(s_device.Component, dump_only=True, many=True)
    parent = NestedOn(s_device.Computer, dump_only=True, description=m.Action.parent_id.comment)
    url = URL(dump_only=True, description=m.Action.url.__doc__)

    @validates_schema
    def validate_times(self, data: dict):
        unix_time = datetime.fromisoformat("1970-01-02 00:00:00+00:00")
        if 'end_time' in data and data['end_time'] < unix_time:
            data['end_time'] = unix_time

        if 'start_time' in data and data['start_time'] < unix_time:
            data['start_time'] = unix_time

        if data.get('end_time') and data.get('start_time'):
            if data['start_time'] > data['end_time']:
                raise ValidationError('The action cannot finish before it starts.')


class ActionWithOneDevice(Action):
    __doc__ = m.ActionWithOneDevice.__doc__
    device = NestedOn(s_device.Device, only_query='id')


class ActionWithMultipleDocuments(Action):
    __doc__ = m.ActionWithMultipleTradeDocuments.__doc__
    documents = NestedOn(s_document.TradeDocument,
                       many=True,
                       required=True,  # todo test ensuring len(devices) >= 1
                       only_query='id',
                       collection_class=OrderedSet)


class ActionWithMultipleDevices(Action):
    __doc__ = m.ActionWithMultipleDevices.__doc__
    devices = NestedOn(s_device.Device,
                       many=True,
                       required=True,  # todo test ensuring len(devices) >= 1
                       only_query='id',
                       collection_class=OrderedSet)


class ActionWithMultipleDevicesCheckingOwner(ActionWithMultipleDevices):

    @post_load
    def check_owner_of_device(self, data):
        for dev in data['devices']:
            if dev.owner != g.user:
                raise ValidationError("Some Devices not exist")


class Add(ActionWithOneDevice):
    __doc__ = m.Add.__doc__


class Remove(ActionWithOneDevice):
    __doc__ = m.Remove.__doc__


class Allocate(ActionWithMultipleDevicesCheckingOwner):
    __doc__ = m.Allocate.__doc__
    start_time = DateTime(data_key='startTime', required=True,
                          description=m.Action.start_time.comment)
    end_time = DateTime(data_key='endTime', required=False,
                        description=m.Action.end_time.comment)
    final_user_code = SanitizedStr(data_key="finalUserCode",
                                   validate=Length(min=1, max=STR_BIG_SIZE),
                                   required=False,
                                   description='This is a internal code for mainteing the secrets of the \
                                               personal datas of the new holder')
    transaction = SanitizedStr(validate=Length(min=1, max=STR_BIG_SIZE),
                        required=False,
                        description='The code used from the owner for \
                                relation with external tool.')
    end_users = Integer(data_key='endUsers', validate=[Range(min=1, error="Value must be greater than 0")])

    @validates_schema
    def validate_allocate(self, data: dict):
        txt = "You need to allocate for a day before today"
        delay = timedelta(days=1)
        today = datetime.now().replace(tzinfo=tzutc()) + delay
        start_time = data['start_time'].replace(tzinfo=tzutc())
        if start_time > today:
            raise ValidationError(txt)

        txt = "You need deallocate before allocate this device again"
        for device in data['devices']:
            if device.allocated:
                raise ValidationError(txt)

            device.allocated = True


class Deallocate(ActionWithMultipleDevicesCheckingOwner):
    __doc__ = m.Deallocate.__doc__
    start_time = DateTime(data_key='startTime', required=True,
                          description=m.Action.start_time.comment)
    transaction = SanitizedStr(validate=Length(min=1, max=STR_BIG_SIZE),
                        required=False,
                        description='The code used from the owner for \
                                relation with external tool.')

    @validates_schema
    def validate_deallocate(self, data: dict):
        txt = "You need to deallocate for a day before today"
        delay = timedelta(days=1)
        today = datetime.now().replace(tzinfo=tzutc()) + delay
        start_time = data['start_time'].replace(tzinfo=tzutc())
        if start_time > today:
            raise ValidationError(txt)

        txt = "Sorry some of this devices are actually deallocate"
        for device in data['devices']:
            if not device.allocated:
                raise ValidationError(txt)

            device.allocated = False


class EraseBasic(ActionWithOneDevice):
    __doc__ = m.EraseBasic.__doc__
    steps = NestedOn('Step', many=True)
    standards = f.List(EnumField(enums.ErasureStandards), dump_only=True)
    certificate = URL(dump_only=True)


class EraseSectors(EraseBasic):
    __doc__ = m.EraseSectors.__doc__


class ErasePhysical(EraseBasic):
    __doc__ = m.ErasePhysical.__doc__
    method = EnumField(PhysicalErasureMethod, description=PhysicalErasureMethod.__doc__)


class Step(Schema):
    __doc__ = m.Step.__doc__
    type = String(description='Only required when it is nested.')
    start_time = DateTime(required=True, data_key='startTime')
    end_time = DateTime(required=True, data_key='endTime')
    severity = EnumField(Severity, description=m.Action.severity.comment)


class StepZero(Step):
    __doc__ = m.StepZero.__doc__


class StepRandom(Step):
    __doc__ = m.StepRandom.__doc__


class Benchmark(ActionWithOneDevice):
    __doc__ = m.Benchmark.__doc__
    elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True)


class BenchmarkDataStorage(Benchmark):
    __doc__ = m.BenchmarkDataStorage.__doc__
    read_speed = Float(required=True, data_key='readSpeed')
    write_speed = Float(required=True, data_key='writeSpeed')


class BenchmarkWithRate(Benchmark):
    __doc__ = m.BenchmarkWithRate.__doc__
    rate = Float(required=True)


class BenchmarkProcessor(BenchmarkWithRate):
    __doc__ = m.BenchmarkProcessor.__doc__


class BenchmarkProcessorSysbench(BenchmarkProcessor):
    __doc__ = m.BenchmarkProcessorSysbench.__doc__


class BenchmarkRamSysbench(BenchmarkWithRate):
    __doc__ = m.BenchmarkRamSysbench.__doc__


class BenchmarkGraphicCard(BenchmarkWithRate):
    __doc__ = m.BenchmarkGraphicCard.__doc__


class Test(ActionWithOneDevice):
    __doc__ = m.Test.__doc__


class MeasureBattery(Test):
    __doc__ = m.MeasureBattery.__doc__
    size = Integer(required=True, description=m.MeasureBattery.size.comment)
    voltage = Integer(required=True, description=m.MeasureBattery.voltage.comment)
    cycle_count = Integer(data_key='cycleCount', description=m.MeasureBattery.cycle_count.comment)
    health = EnumField(enums.BatteryHealth, description=m.MeasureBattery.health.comment)


class TestDataStorage(Test):
    __doc__ = m.TestDataStorage.__doc__
    elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True)
    length = EnumField(TestDataStorageLength, required=True)
    status = SanitizedStr(lower=True, validate=Length(max=STR_SIZE), required=True)
    lifetime = TimeDelta(precision=TimeDelta.HOURS)
    power_on_hours = Integer(data_key='powerOnHours', dump_only=True)
    assessment = Boolean()
    reallocated_sector_count = Integer(data_key='reallocatedSectorCount')
    power_cycle_count = Integer(data_key='powerCycleCount')
    reported_uncorrectable_errors = Integer(data_key='reportedUncorrectableErrors')
    command_timeout = Integer(data_key='commandTimeout')
    current_pending_sector_count = Integer(data_key='currentPendingSectorCount')
    offline_uncorrectable = Integer(data_key='offlineUncorrectable')
    remaining_lifetime_percentage = Integer(data_key='remainingLifetimePercentage')


class StressTest(Test):
    __doc__ = m.StressTest.__doc__
    elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True)


class TestAudio(Test):
    __doc__ = m.TestAudio.__doc__
    speaker = Boolean(description=m.TestAudio._speaker.comment)
    microphone = Boolean(description=m.TestAudio._microphone.comment)


class TestConnectivity(Test):
    __doc__ = m.TestConnectivity.__doc__


class TestCamera(Test):
    __doc__ = m.TestCamera.__doc__


class TestKeyboard(Test):
    __doc__ = m.TestKeyboard.__doc__


class TestTrackpad(Test):
    __doc__ = m.TestTrackpad.__doc__


class TestBios(Test):
    __doc__ = m.TestBios.__doc__
    bios_power_on = Boolean()
    access_range = EnumField(BiosAccessRange, data_key='accessRange')


class VisualTest(Test):
    __doc__ = m.VisualTest.__doc__
    appearance_range = EnumField(AppearanceRange, data_key='appearanceRange')
    functionality_range = EnumField(FunctionalityRange,
                                    data_key='functionalityRange')
    labelling = Boolean()


class Rate(ActionWithOneDevice):
    __doc__ = m.Rate.__doc__
    rating = Integer(validate=Range(*R_POSITIVE),
                     dump_only=True,
                     description=m.Rate._rating.comment)
    version = Version(dump_only=True,
                      description=m.Rate.version.comment)
    appearance = Integer(validate=Range(enums.R_NEGATIVE),
                         dump_only=True,
                         description=m.Rate._appearance.comment)
    functionality = Integer(validate=Range(enums.R_NEGATIVE),
                            dump_only=True,
                            description=m.Rate._functionality.comment)
    rating_range = EnumField(RatingRange,
                             dump_only=True,
                             data_key='ratingRange',
                             description=m.Rate.rating_range.__doc__)


class RateComputer(Rate):
    __doc__ = m.RateComputer.__doc__
    processor = Float(dump_only=True)
    ram = Float(dump_only=True)
    data_storage = Float(dump_only=True, data_key='dataStorage')
    graphic_card = Float(dump_only=True, data_key='graphicCard')

    data_storage_range = EnumField(RatingRange, dump_only=True, data_key='dataStorageRange')
    ram_range = EnumField(RatingRange, dump_only=True, data_key='ramRange')
    processor_range = EnumField(RatingRange, dump_only=True, data_key='processorRange')
    graphic_card_range = EnumField(RatingRange, dump_only=True, data_key='graphicCardRange')


class Price(ActionWithOneDevice):
    __doc__ = m.Price.__doc__
    currency = EnumField(Currency, required=True, description=m.Price.currency.comment)
    price = Decimal(places=m.Price.SCALE,
                    rounding=m.Price.ROUND,
                    required=True,
                    description=m.Price.price.comment)
    version = Version(dump_only=True, description=m.Price.version.comment)
    rating = NestedOn(Rate, dump_only=True, description=m.Price.rating_id.comment)


class EreusePrice(Price):
    __doc__ = m.EreusePrice.__doc__

    class Service(MarshmallowSchema):
        class Type(MarshmallowSchema):
            amount = Float()
            percentage = Float()

        standard = Nested(Type)
        warranty2 = Nested(Type)

    warranty2 = Float()
    refurbisher = Nested(Service)
    retailer = Nested(Service)
    platform = Nested(Service)


class Install(ActionWithOneDevice):
    __doc__ = m.Install.__doc__
    name = SanitizedStr(validate=Length(min=4, max=STR_BIG_SIZE),
                        required=True,
                        description='The name of the OS installed.')
    elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True)
    address = Integer(validate=OneOf({8, 16, 32, 64, 128, 256}))


class Snapshot(ActionWithOneDevice):
    __doc__ = m.Snapshot.__doc__
    """
    The Snapshot updates the state of the device with information about
    its components and actions performed at them.

    See docs for more info.
    """
    uuid = UUID()
    software = EnumField(SnapshotSoftware,
                         required=True,
                         description='The software that generated this Snapshot.')
    version = Version(required=True, description='The version of the software.')
    actions = NestedOn(Action, many=True, dump_only=True)
    elapsed = TimeDelta(precision=TimeDelta.SECONDS)
    components = NestedOn(s_device.Component,
                          many=True,
                          description='A list of components that are inside of the device'
                                      'at the moment of this Snapshot.'
                                      'Order is preserved, so the component num 0 when'
                                      'submitting is the component num 0 when returning it back.')

    @validates_schema
    def validate_workbench_version(self, data: dict):
        if data['software'] == SnapshotSoftware.Workbench:
            if data['version'] < app.config['MIN_WORKBENCH']:
                raise ValidationError(
                    'Min. supported Workbench version is '
                    '{} but yours is {}.'.format(app.config['MIN_WORKBENCH'], data['version']),
                    field_names=['version']
                )

    @validates_schema
    def validate_components_only_workbench(self, data: dict):
        if (data['software'] != SnapshotSoftware.Workbench) and (data['software'] != SnapshotSoftware.WorkbenchAndroid):
            if data.get('components', None) is not None:
                raise ValidationError('Only Workbench can add component info',
                                      field_names=['components'])

    @validates_schema
    def validate_only_workbench_fields(self, data: dict):
        """Ensures workbench has ``elapsed`` and ``uuid`` and no others."""
        # todo test
        if data['software'] == SnapshotSoftware.Workbench:
            if not data.get('uuid', None):
                raise ValidationError('Snapshots from Workbench and WorkbenchAndroid must have uuid',
                                      field_names=['uuid'])
            if data.get('elapsed', None) is None:
                raise ValidationError('Snapshots from Workbench must have elapsed',
                                      field_names=['elapsed'])
        elif data['software'] == SnapshotSoftware.WorkbenchAndroid:
            if not data.get('uuid', None):
                raise ValidationError('Snapshots from Workbench and WorkbenchAndroid must have uuid',
                                      field_names=['uuid'])
        else:
            if data.get('uuid', None):
                raise ValidationError('Only Snapshots from Workbench or WorkbenchAndroid can have uuid',
                                      field_names=['uuid'])
            if data.get('elapsed', None):
                raise ValidationError('Only Snapshots from Workbench can have elapsed',
                                      field_names=['elapsed'])


class ToRepair(ActionWithMultipleDevicesCheckingOwner):
    __doc__ = m.ToRepair.__doc__


class Repair(ActionWithMultipleDevicesCheckingOwner):
    __doc__ = m.Repair.__doc__


class Ready(ActionWithMultipleDevicesCheckingOwner):
    __doc__ = m.Ready.__doc__


class ActionStatus(Action):
    rol_user = NestedOn(s_user.User, dump_only=True, exclude=('token',))
    devices = NestedOn(s_device.Device,
                       many=True,
                       required=False,  # todo test ensuring len(devices) >= 1
                       only_query='id',
                       collection_class=OrderedSet)
    documents = NestedOn(s_document.TradeDocument,
                       many=True,
                       required=False,  # todo test ensuring len(devices) >= 1
                       only_query='id',
                       collection_class=OrderedSet)

    @pre_load
    def put_devices(self, data: dict):
        if 'devices' not in data.keys():
            data['devices'] = []

    @post_load
    def put_rol_user(self, data: dict):
        for dev in data['devices']:
            trades = [ac for ac in dev.actions if ac.t == 'Trade']
            if not trades:
                return data

            trade = trades[-1]

            if trade.user_from == g.user:
                data['rol_user'] = trade.user_to
            data['trade'] = trade


class Recycling(ActionStatus):
    __doc__ = m.Recycling.__doc__


class Use(ActionStatus):
    __doc__ = m.Use.__doc__


class Refurbish(ActionStatus):
    __doc__ = m.Refurbish.__doc__


class Management(ActionStatus):
    __doc__ = m.Management.__doc__


class ToPrepare(ActionWithMultipleDevicesCheckingOwner):
    __doc__ = m.ToPrepare.__doc__


class Prepare(ActionWithMultipleDevicesCheckingOwner):
    __doc__ = m.Prepare.__doc__


class DataWipe(ActionWithMultipleDevicesCheckingOwner):
    __doc__ = m.DataWipe.__doc__
    document = NestedOn(s_generic_document.DataWipeDocument, only_query='id')


class Live(ActionWithOneDevice):
    __doc__ = m.Live.__doc__
    """
    The Snapshot updates the state of the device with information about
    its components and actions performed at them.

    See docs for more info.
    """
    uuid = UUID()
    software = EnumField(SnapshotSoftware,
                         required=True,
                         description='The software that generated this Snapshot.')
    version = Version(required=True, description='The version of the software.')
    final_user_code = SanitizedStr(data_key="finalUserCode", dump_only=True)
    licence_version = Version(required=True, description='The version of the software.')
    components = NestedOn(s_device.Component,
                          many=True,
                          description='A list of components that are inside of the device'
                                      'at the moment of this Snapshot.'
                                      'Order is preserved, so the component num 0 when'
                                      'submitting is the component num 0 when returning it back.')
    usage_time_allocate = TimeDelta(data_key='usageTimeAllocate', required=False,
                                    precision=TimeDelta.HOURS, dump_only=True)


class Organize(ActionWithMultipleDevices):
    __doc__ = m.Organize.__doc__


class Reserve(Organize):
    __doc__ = m.Reserve.__doc__


class CancelReservation(Organize):
    __doc__ = m.CancelReservation.__doc__


class Confirm(ActionWithMultipleDevices):
    __doc__ = m.Confirm.__doc__
    action = NestedOn('Action', only_query='id')

    @validates_schema
    def validate_revoke(self, data: dict):
        for dev in data['devices']:
            # if device not exist in the Trade, then this query is wrong
            if dev not in data['action'].devices:
                txt = "Device {} not exist in the trade".format(dev.devicehub_id)
                raise ValidationError(txt)


class Revoke(ActionWithMultipleDevices):
    __doc__ = m.Revoke.__doc__
    action = NestedOn('Action', only_query='id')

    @validates_schema
    def validate_revoke(self, data: dict):
        for dev in data['devices']:
            # if device not exist in the Trade, then this query is wrong
            if dev not in data['action'].devices:
                txt = "Device {} not exist in the trade".format(dev.devicehub_id)
                raise ValidationError(txt)

        for doc in data.get('documents', []):
            # if document not exist in the Trade, then this query is wrong
            if doc not in data['action'].documents:
                txt = "Document {} not exist in the trade".format(doc.file_name)
                raise ValidationError(txt)

    @validates_schema
    def validate_documents(self, data):
        """Check if there are or no one before confirmation,
           This is not checked in the view becouse the list of documents is inmutable

        """
        if not data['devices'] == OrderedSet():
            return

        documents = []
        for doc in data['documents']:
            actions = copy.copy(doc.actions)
            actions.reverse()
            for ac in actions:
                if ac == data['action']:
                    # data['action'] is a Trade action, if this is the first action
                    # to find mean that this document don't have a confirmation
                    break

                if ac.t == 'Revoke' and ac.user == g.user:
                    # this doc is confirmation jet
                    break

                if ac.t == Confirm.t and ac.user == g.user:
                    documents.append(doc)
                    break

        if not documents:
            txt = 'No there are documents to revoke'
            raise ValidationError(txt)


class ConfirmRevoke(Revoke):
    pass


class ConfirmDocument(ActionWithMultipleDocuments):
    __doc__ = m.Confirm.__doc__
    action = NestedOn('Action', only_query='id')

    @validates_schema
    def validate_documents(self, data):
        """If there are one device than have one confirmation,
           then remove the list this device of the list of devices of this action
        """
        if data['documents'] == OrderedSet():
            return

        for doc in data['documents']:
            if not doc.lot.trade:
                return

            data['action'] = doc.lot.trade

            if not doc.actions:
                continue

            if not doc.trading == 'Need Confirmation':
                txt = 'No there are documents to confirm'
                raise ValidationError(txt)


class RevokeDocument(ActionWithMultipleDocuments):
    __doc__ = m.RevokeDocument.__doc__
    action = NestedOn('Action', only_query='id')

    @validates_schema
    def validate_documents(self, data):
        """Check if there are or no one before confirmation,
           This is not checked in the view becouse the list of documents is inmutable

        """
        if data['documents'] == OrderedSet():
            return

        for doc in data['documents']:
            if not doc.lot.trade:
                return

            data['action'] = doc.lot.trade

            if not doc.actions:
                continue

            if doc.trading not in ['Document Confirmed', 'Confirm']:
                txt = 'No there are documents to revoke'
                raise ValidationError(txt)


class ConfirmRevokeDocument(ActionWithMultipleDocuments):
    __doc__ = m.ConfirmRevokeDocument.__doc__
    action = NestedOn('Action', only_query='id')

    @validates_schema
    def validate_documents(self, data):
        """Check if there are or no one before confirmation,
           This is not checked in the view becouse the list of documents is inmutable

        """
        if data['documents'] == OrderedSet():
            return

        for doc in data['documents']:
            if not doc.lot.trade:
                return

            if not doc.actions:
                continue

            if not doc.trading == 'Revoke':
                txt = 'No there are documents with revoke for confirm'
                raise ValidationError(txt)

            data['action'] = doc.actions[-1]


class Trade(ActionWithMultipleDevices):
    __doc__ = m.Trade.__doc__
    date = DateTime(data_key='date', required=False)
    price = Float(required=False, data_key='price')
    user_to_email = SanitizedStr(
        validate=Length(max=STR_SIZE),
        data_key='userToEmail',
        missing='',
        required=False
    )
    user_to = NestedOn(s_user.User, dump_only=True, data_key='userTo')
    user_from_email = SanitizedStr(
        validate=Length(max=STR_SIZE),
        data_key='userFromEmail',
        missing='',
        required=False
    )
    user_from = NestedOn(s_user.User, dump_only=True, data_key='userFrom')
    code = SanitizedStr(validate=Length(max=STR_SIZE), data_key='code', required=False)
    confirm = Boolean(
        data_key='confirms',
        missing=True,
        description="""If you need confirmation of the user you need actevate this field"""
    )
    lot = NestedOn('Lot',
                   many=False,
                   required=True,
                   only_query='id')

    @pre_load
    def adding_devices(self, data: dict):
        if not 'devices' in data.keys():
            data['devices'] = []

    @validates_schema
    def validate_lot(self, data: dict):
        if not g.user.email in [data['user_from_email'], data['user_to_email']]:
            txt = "you need to be one of the users of involved in the Trade"
            raise ValidationError(txt)

        for dev in data['lot'].devices:
            if not dev.owner == g.user:
                txt = "you need to be the owner of the devices for to do a trade"
                raise ValidationError(txt)

        if not data['lot'].owner == g.user:
            txt = "you need to be the owner of the lot for to do a trade"
            raise ValidationError(txt)

        for doc in data['lot'].documents:
            if not doc.owner == g.user:
                txt = "you need to be the owner of the documents for to do a trade"
                raise ValidationError(txt)

        data['devices'] = data['lot'].devices
        data['documents'] = data['lot'].documents

    @validates_schema
    def validate_user_to_email(self, data: dict):
        """
        - if user_to exist
            * confirmation
            * without confirmation
        - if user_to don't exist
            * without confirmation

        """
        if data['user_to_email']:
            user_to = User.query.filter_by(email=data['user_to_email']).one()
            data['user_to'] = user_to
        else:
            data['confirm'] = False

    @validates_schema
    def validate_user_from_email(self, data: dict):
        """
        - if user_from exist
            * confirmation
            * without confirmation
        - if user_from don't exist
            * without confirmation

        """
        if data['user_from_email']:
            user_from = User.query.filter_by(email=data['user_from_email']).one()
            data['user_from'] = user_from

    @validates_schema
    def validate_email_users(self, data: dict):
        """We need at least one user"""
        confirm = data['confirm']
        user_from = data['user_from_email']
        user_to = data['user_to_email']

        if not (user_from or user_to):
            txt = "you need one user from or user to for to do a trade"
            raise ValidationError(txt)

        if confirm and not (user_from and user_to):
            txt = "you need one user for to do a trade"
            raise ValidationError(txt)

        if not g.user.email in [user_from, user_to]:
            txt = "you need to be one of participate of the action"
            raise ValidationError(txt)

    @validates_schema
    def validate_code(self, data: dict):
        """If the user not exist, you need a code to be able to do the traceability"""
        if data['user_from_email'] and data['user_to_email']:
            data['confirm'] = True
            return

        if not data['confirm'] and not data.get('code'):
            txt = "you need a code to be able to do the traceability"
            raise ValidationError(txt)

        if not data['confirm']:
            data['code'] = data['code'].replace('@', '_')


class InitTransfer(Trade):
    __doc__ = m.InitTransfer.__doc__


class Sell(Trade):
    __doc__ = m.Sell.__doc__


class Donate(Trade):
    __doc__ = m.Donate.__doc__


class Rent(Trade):
    __doc__ = m.Rent.__doc__


class MakeAvailable(ActionWithMultipleDevices):
    __doc__ = m.MakeAvailable.__doc__


class CancelTrade(Trade):
    __doc__ = m.CancelTrade.__doc__


class ToDisposeProduct(Trade):
    __doc__ = m.ToDisposeProduct.__doc__


class DisposeProduct(Trade):
    __doc__ = m.DisposeProduct.__doc__


class TransferOwnershipBlockchain(Trade):
    __doc__ = m.TransferOwnershipBlockchain.__doc__


class Delete(ActionWithMultipleDevicesCheckingOwner):
    __doc__ = m.Delete.__doc__

    @post_load
    def deactivate_device(self, data):
        for dev in data['devices']:
            if dev.last_action_trading is None:
                dev.active = False


class Migrate(ActionWithMultipleDevices):
    __doc__ = m.Migrate.__doc__
    other = URL()


class MigrateTo(Migrate):
    __doc__ = m.MigrateTo.__doc__


class MigrateFrom(Migrate):
    __doc__ = m.MigrateFrom.__doc__


class MoveOnDocument(Action):
    __doc__ = m.MoveOnDocument.__doc__
    weight = Integer()
    container_from = NestedOn('TradeDocument', only_query='id')
    container_to = NestedOn('TradeDocument', only_query='id')

    @pre_load
    def extract_container(self, data):
        id_hash = data['container_to']
        docs = TradeDocument.query.filter_by(owner=g.user, file_hash=id_hash).all()
        if len(docs) > 1:
            txt = 'This document it is associated in more than one lot'
            raise ValidationError(txt)

        if len(docs) < 1:
            txt = 'This document not exist'
            raise ValidationError(txt)
        data['container_to'] = docs[0].id

    @post_load
    def adding_documents(self, data):
        """Adding action in the 2 TradeDocuments"""
        docs = OrderedSet()
        docs.add(data['container_to'])
        docs.add(data['container_from'])
        data['documents'] = docs