package be.seeseepuff.allowanceplanner.service;

import be.seeseepuff.allowanceplanner.dto.*;
import be.seeseepuff.allowanceplanner.entity.Allowance;
import be.seeseepuff.allowanceplanner.entity.History;
import be.seeseepuff.allowanceplanner.entity.User;
import be.seeseepuff.allowanceplanner.repository.AllowanceRepository;
import be.seeseepuff.allowanceplanner.repository.HistoryRepository;
import be.seeseepuff.allowanceplanner.repository.UserRepository;
import be.seeseepuff.allowanceplanner.util.ColourUtil;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Service
public class AllowanceService {
    private final AllowanceRepository allowanceRepository;
    private final UserRepository userRepository;
    private final HistoryRepository historyRepository;

    public AllowanceService(AllowanceRepository allowanceRepository,
                            UserRepository userRepository,
                            HistoryRepository historyRepository) {
        this.allowanceRepository = allowanceRepository;
        this.userRepository = userRepository;
        this.historyRepository = historyRepository;
    }

    public List<AllowanceDto> getUserAllowances(int userId) {
        User user = userRepository.findById(userId).orElseThrow();
        List<AllowanceDto> result = new ArrayList<>();

        // Add the "rest" allowance (id=0)
        result.add(new AllowanceDto(0, "", 0, user.getBalance() / 100.0, user.getWeight(), ""));

        // Add named allowances
        for (Allowance a : allowanceRepository.findByUserIdOrderByIdAsc(userId)) {
            result.add(toDto(a));
        }
        return result;
    }

    public Optional<AllowanceDto> getUserAllowanceById(int userId, int allowanceId) {
        if (allowanceId == 0) {
            return userRepository.findById(userId)
                    .map(u -> new AllowanceDto(0, "", 0, u.getBalance() / 100.0, u.getWeight(), ""));
        }
        return allowanceRepository.findByIdAndUserId(allowanceId, userId)
                .map(this::toDto);
    }

    @Transactional
    public int createAllowance(int userId, CreateAllowanceRequest request) {
        int colour = ColourUtil.convertStringToColour(request.colour());
        Allowance allowance = new Allowance();
        allowance.setUserId(userId);
        allowance.setName(request.name());
        allowance.setTarget(Math.round(request.target() * 100.0));
        allowance.setWeight(request.weight());
        allowance.setColour(colour);
        allowance = allowanceRepository.save(allowance);
        return allowance.getId();
    }

    @Transactional
    public boolean deleteAllowance(int userId, int allowanceId) {
        int count = allowanceRepository.countByIdAndUserId(allowanceId, userId);
        if (count == 0) {
            return false;
        }
        allowanceRepository.deleteByIdAndUserId(allowanceId, userId);
        return true;
    }

    @Transactional
    public boolean updateAllowance(int userId, int allowanceId, UpdateAllowanceRequest request) {
        if (allowanceId == 0) {
            User user = userRepository.findById(userId).orElseThrow();
            user.setWeight(request.weight());
            userRepository.save(user);
            return true;
        }

        Optional<Allowance> opt = allowanceRepository.findByIdAndUserId(allowanceId, userId);
        if (opt.isEmpty()) {
            return false;
        }

        int colour = ColourUtil.convertStringToColour(request.colour());
        Allowance allowance = opt.get();
        allowance.setName(request.name());
        allowance.setTarget(Math.round(request.target() * 100.0));
        allowance.setWeight(request.weight());
        allowance.setColour(colour);
        allowanceRepository.save(allowance);
        return true;
    }

    @Transactional
    public void bulkUpdateAllowance(int userId, List<BulkUpdateAllowanceRequest> requests) {
        for (BulkUpdateAllowanceRequest req : requests) {
            if (req.id() == 0) {
                User user = userRepository.findById(userId).orElseThrow();
                user.setWeight(req.weight());
                userRepository.save(user);
            } else {
                allowanceRepository.findByIdAndUserId(req.id(), userId).ifPresent(a ->
                {
                    a.setWeight(req.weight());
                    allowanceRepository.save(a);
                });
            }
        }
    }

    @Transactional
    public boolean completeAllowance(int userId, int allowanceId) {
        Optional<Allowance> opt = allowanceRepository.findByIdAndUserId(allowanceId, userId);
        if (opt.isEmpty()) {
            return false;
        }

        Allowance allowance = opt.get();
        long cost = allowance.getBalance();
        String allowanceName = allowance.getName();

        // Delete the allowance
        allowanceRepository.delete(allowance);

        // Add a history entry
        History history = new History();
        history.setUserId(userId);
        history.setTimestamp(Instant.now().getEpochSecond());
        history.setAmount(-cost);
        history.setDescription("Allowance completed: " + allowanceName);
        historyRepository.save(history);

        return true;
    }

    @Transactional
    public boolean addAllowanceAmount(int userId, int allowanceId, AddAllowanceAmountRequest request) {
        long remainingAmount = Math.round(request.amount() * 100);

        // Insert history entry
        History history = new History();
        history.setUserId(userId);
        history.setTimestamp(Instant.now().getEpochSecond());
        history.setAmount(remainingAmount);
        history.setDescription(request.description());
        historyRepository.save(history);

        if (allowanceId == 0) {
            if (remainingAmount < 0) {
                User user = userRepository.findById(userId).orElseThrow();
                if (remainingAmount > user.getBalance()) {
                    throw new IllegalArgumentException("cannot remove more than the current balance: " + user.getBalance());
                }
            }
            User user = userRepository.findById(userId).orElseThrow();
            user.setBalance(user.getBalance() + remainingAmount);
            userRepository.save(user);
        } else if (remainingAmount < 0) {
            Allowance allowance = allowanceRepository.findByIdAndUserId(allowanceId, userId).orElse(null);
            if (allowance == null) {
                return false;
            }
            if (remainingAmount > allowance.getBalance()) {
                throw new IllegalArgumentException("cannot remove more than the current allowance balance: " + allowance.getBalance());
            }
            allowance.setBalance(allowance.getBalance() + remainingAmount);
            allowanceRepository.save(allowance);
        } else {
            Allowance allowance = allowanceRepository.findByIdAndUserId(allowanceId, userId).orElse(null);
            if (allowance == null) {
                return false;
            }

            long toAdd = remainingAmount;
            if (allowance.getBalance() + toAdd > allowance.getTarget()) {
                toAdd = allowance.getTarget() - allowance.getBalance();
            }
            remainingAmount -= toAdd;

            if (toAdd > 0) {
                allowance.setBalance(allowance.getBalance() + toAdd);
                allowanceRepository.save(allowance);
            }

            if (remainingAmount > 0) {
                addDistributedReward(userId, (int) remainingAmount);
            }
        }
        return true;
    }

    public void addDistributedReward(int userId, int reward) {
        User user = userRepository.findById(userId).orElseThrow();
        double userWeight = user.getWeight();

        double sumOfWeights = allowanceRepository.sumPositiveWeights(userId) + userWeight;

        int remainingReward = reward;

        if (sumOfWeights > 0) {
            List<Allowance> allowances = allowanceRepository.findByUserIdWithPositiveWeightOrderByRemainingAsc(userId);
            for (Allowance allowance : allowances) {
                int amount = (int) ((allowance.getWeight() / sumOfWeights) * remainingReward);
                if (allowance.getBalance() + amount > allowance.getTarget()) {
                    amount = (int) (allowance.getTarget() - allowance.getBalance());
                }
                sumOfWeights -= allowance.getWeight();
                allowance.setBalance(allowance.getBalance() + amount);
                allowanceRepository.save(allowance);
                remainingReward -= amount;
            }
        }

        // Add remaining to user's balance
        user = userRepository.findById(userId).orElseThrow();
        user.setBalance(user.getBalance() + remainingReward);
        userRepository.save(user);
    }

    public List<HistoryDto> getHistory(int userId) {
        return historyRepository.findByUserIdOrderByIdDesc(userId).stream()
                .map(h -> new HistoryDto(
                        h.getAmount() / 100.0,
                        Instant.ofEpochSecond(h.getTimestamp()),
                        h.getDescription()))
                .toList();
    }

    @Transactional
    public void addHistory(int userId, PostHistoryRequest request) {
        long amount = Math.round(request.allowance() * 100.0);
        History history = new History();
        history.setUserId(userId);
        history.setTimestamp(Instant.now().getEpochSecond());
        history.setAmount(amount);
        history.setDescription(request.description());
        historyRepository.save(history);
    }

    private AllowanceDto toDto(Allowance a) {
        return new AllowanceDto(
                a.getId(),
                a.getName(),
                a.getTarget() / 100.0,
                a.getBalance() / 100.0,
                a.getWeight(),
                ColourUtil.convertColourToString(a.getColour()));
    }
}
