import attr
import logging
from duniterpy.documents import (
    Identity,
    Membership,
    BlockUID,
    Certification,
    OutputSource,
    Transaction,
    Unlock,
    SIGParameter,
)
from duniterpy.documents.transaction import reduce_base
from duniterpy.grammars import output
from duniterpy.key import SigningKey
from duniterpy.key.scrypt_params import ScryptParams


def outputs_from_sources(amount, sources):
    # such a dirty algorithmm
    # shamelessly copy pasted from sakia
    def current_value(inputs, overhs):
        i = 0
        for s in inputs:
            i += s.amount * (10 ** s.base)
        for o in overhs:
            i -= o[0] * (10 ** o[1])
        return i

    amount, amount_base = reduce_base(amount, 0)
    current_base = max([src.base for src in sources])
    result_sources = []
    outputs = []
    overheads = []
    buf_sources = list(sources)
    while current_base >= 0:
        for s in [src for src in buf_sources if src.base == current_base]:
            test_sources = result_sources + [s]
            # if we have to compute an overhead
            if current_value(test_sources, overheads) > amount * (10 ** amount_base):
                overhead = current_value(test_sources, overheads) - int(amount) * (
                    10 ** amount_base
                )
                # we round the overhead in the current base
                # exemple : 12 in base 1 -> 1*10^1
                overhead = int(round(float(overhead) / (10 ** current_base)))
                source_value = s.amount * (10 ** s.base)
                out = int(
                    (source_value - (overhead * (10 ** current_base)))
                    / (10 ** current_base)
                )
                if out * (10 ** current_base) <= amount * (10 ** amount_base):
                    result_sources.append(s)
                    buf_sources.remove(s)
                    overheads.append((overhead, current_base))
                    outputs.append((out, current_base))
            # else just add the output
            else:
                result_sources.append(s)
                buf_sources.remove(s)
                outputs.append((s.amount, s.base))
            if current_value(result_sources, overheads) == amount * (10 ** amount_base):
                return result_sources, outputs, overheads

        current_base -= 1
    raise ValueError("Not enough sources")


@attr.s()
class User:
    """
    The user generates identities documents and sign them
    """

    SCRYPT_PARAMS = ScryptParams(2 ** 12, 16, 1)

    currency = attr.ib(validator=attr.validators.instance_of(str))
    uid = attr.ib(validator=attr.validators.instance_of(str))
    key = attr.ib(validator=attr.validators.instance_of(SigningKey))
    salt = attr.ib(validator=attr.validators.instance_of(str))
    password = attr.ib(validator=attr.validators.instance_of(str))
    blockstamp = attr.ib(validator=attr.validators.instance_of(BlockUID))
    _logger = attr.ib(default=attr.Factory(lambda: logging.getLogger("mirage")))

    @classmethod
    def create(cls, currency, uid, salt, password, blockstamp):
        return cls(
            currency,
            uid,
            SigningKey.from_credentials(salt, password, User.SCRYPT_PARAMS),
            salt,
            password,
            blockstamp,
        )

    def identity(self):
        identity = Identity(
            10, self.currency, self.key.pubkey, self.uid, self.blockstamp, None
        )
        identity.sign([self.key])

        return identity

    def join(self, blockstamp):
        ms_doc = Membership(
            10,
            self.currency,
            self.key.pubkey,
            blockstamp,
            "IN",
            self.uid,
            self.blockstamp,
            None,
        )
        ms_doc.sign([self.key])
        return ms_doc

    def leave(self, blockstamp):
        ms_doc = Membership(
            10,
            self.currency,
            self.key.pubkey,
            blockstamp,
            "OUT",
            self.uid,
            self.blockstamp,
            None,
        )
        ms_doc.sign([self.key])
        return ms_doc

    def certify(self, other, blockstamp):
        cert = Certification(
            10, self.currency, self.key.pubkey, other.identity(), blockstamp, None
        )
        cert.sign([self.key])
        return cert

    def tx_outputs(self, receiver, outputs, overheads):
        total = []
        outputs_bases = set(o[1] for o in outputs)
        for base in outputs_bases:
            output_sum = 0
            for o in outputs:
                if o[1] == base:
                    output_sum += o[0]
            total.append(
                OutputSource(
                    output_sum,
                    base,
                    output.Condition.token(
                        output.SIG.token(receiver.key.pubkey)
                    ).compose(output.Condition()),
                )
            )

        overheads_bases = set(o[1] for o in overheads)
        for base in overheads_bases:
            overheads_sum = 0
            for o in overheads:
                if o[1] == base:
                    overheads_sum += o[0]
            total.append(
                OutputSource(
                    overheads_sum,
                    base,
                    output.Condition.token(output.SIG.token(self.key.pubkey)).compose(
                        output.Condition()
                    ),
                )
            )

        return total

    def send_money(self, amount, sources, receiver, blockstamp, message):

        result = outputs_from_sources(amount, sources)
        inputs = result[0]
        computed_outputs = result[1]
        overheads = result[2]

        unlocks = []
        for i, _ in enumerate(sources):
            unlocks.append(Unlock(i, [SIGParameter(0)]))
        outputs = self.tx_outputs(receiver, computed_outputs, overheads)

        tx = Transaction(
            3,
            self.currency,
            blockstamp,
            0,
            [self.key.pubkey],
            inputs,
            unlocks,
            outputs,
            message,
            [],
        )
        tx.sign([self.key])
        return tx
