package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.entity.Budget;
import dev.lions.unionflow.server.entity.BudgetLine;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.BudgetRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import dev.lions.unionflow.server.api.dto.finance_workflow.request.CreateBudgetLineRequest;
import dev.lions.unionflow.server.api.dto.finance_workflow.request.CreateBudgetRequest;
import dev.lions.unionflow.server.api.dto.finance_workflow.response.BudgetLineResponse;
import dev.lions.unionflow.server.api.dto.finance_workflow.response.BudgetResponse;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.BadRequestException;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.jboss.logging.Logger;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Service métier pour la gestion des budgets
 *
 * @author UnionFlow Team
 * @version 1.0
 * @since 2026-03-13
 */
@ApplicationScoped
public class BudgetService {

    private static final Logger LOG = Logger.getLogger(BudgetService.class);

    @Inject
    BudgetRepository budgetRepository;

    @Inject
    OrganisationRepository organisationRepository;

    @Inject
    JsonWebToken jwt;

    /**
     * Récupère tous les budgets d'une organisation avec filtres optionnels
     */
    public List<BudgetResponse> getBudgets(UUID organizationId, String status, Integer year) {
        LOG.infof("Récupération des budgets pour l'organisation %s (status=%s, year=%s)",
            organizationId, status, year);

        if (organizationId == null) {
            throw new BadRequestException("L'ID de l'organisation est requis");
        }

        List<Budget> budgets = budgetRepository.findByOrganisationWithFilters(
            organizationId, status, year);

        return budgets.stream()
            .map(this::toResponse)
            .collect(Collectors.toList());
    }

    /**
     * Récupère un budget par ID
     */
    public BudgetResponse getBudgetById(UUID budgetId) {
        LOG.infof("Récupération du budget %s", budgetId);

        Budget budget = budgetRepository.findByIdOptional(budgetId)
            .orElseThrow(() -> new NotFoundException("Budget non trouvé: " + budgetId));

        return toResponse(budget);
    }

    /**
     * Crée un nouveau budget
     */
    @Transactional
    public BudgetResponse createBudget(CreateBudgetRequest request) {
        LOG.infof("Création d'un budget: %s", request.getName());

        // Vérifier que l'organisation existe
        Organisation organisation = organisationRepository.findByIdOptional(request.getOrganizationId())
            .orElseThrow(() -> new NotFoundException("Organisation non trouvée: " + request.getOrganizationId()));

        // Valider la période
        if ("MONTHLY".equals(request.getPeriod()) && request.getMonth() == null) {
            throw new BadRequestException("Le mois est requis pour un budget mensuel");
        }

        // Calculer les dates de début et fin
        LocalDate startDate = calculateStartDate(request.getPeriod(), request.getYear(), request.getMonth());
        LocalDate endDate = calculateEndDate(request.getPeriod(), request.getYear(), request.getMonth());

        // Récupérer l'utilisateur courant
        UUID userId = UUID.fromString(jwt.getClaim("sub"));

        // Créer le budget
        Budget budget = Budget.builder()
            .name(request.getName())
            .description(request.getDescription())
            .organisation(organisation)
            .period(request.getPeriod())
            .year(request.getYear())
            .month(request.getMonth())
            .status("DRAFT")
            .currency(request.getCurrency() != null ? request.getCurrency() : "XOF")
            .createdById(userId)
            .createdAtBudget(LocalDateTime.now())
            .startDate(startDate)
            .endDate(endDate)
            .build();

        // Ajouter les lignes budgétaires
        for (CreateBudgetLineRequest lineRequest : request.getLines()) {
            BudgetLine line = BudgetLine.builder()
                .budget(budget)
                .category(lineRequest.getCategory())
                .name(lineRequest.getName())
                .description(lineRequest.getDescription())
                .amountPlanned(lineRequest.getAmountPlanned())
                .amountRealized(BigDecimal.ZERO)
                .notes(lineRequest.getNotes())
                .build();

            budget.addLine(line);
        }

        // Persister le budget
        budgetRepository.persist(budget);

        LOG.infof("Budget créé avec ID: %s", budget.getId());

        return toResponse(budget);
    }

    /**
     * Récupère le suivi budgétaire (tracking)
     */
    public Map<String, Object> getBudgetTracking(UUID budgetId) {
        LOG.infof("Récupération du suivi budgétaire pour %s", budgetId);

        Budget budget = budgetRepository.findByIdOptional(budgetId)
            .orElseThrow(() -> new NotFoundException("Budget non trouvé: " + budgetId));

        Map<String, Object> tracking = new HashMap<>();
        tracking.put("budgetId", budget.getId());
        tracking.put("name", budget.getName());
        tracking.put("status", budget.getStatus());
        tracking.put("totalPlanned", budget.getTotalPlanned());
        tracking.put("totalRealized", budget.getTotalRealized());
        tracking.put("realizationRate", budget.getRealizationRate());
        tracking.put("variance", budget.getVariance());
        tracking.put("isOverBudget", budget.isOverBudget());
        tracking.put("isActive", budget.isActive());
        tracking.put("isCurrentPeriod", budget.isCurrentPeriod());

        // Tracking par catégorie
        Map<String, Map<String, Object>> byCategory = new HashMap<>();
        for (BudgetLine line : budget.getLines()) {
            String category = line.getCategory();
            Map<String, Object> categoryData = byCategory.getOrDefault(category, new HashMap<>());

            BigDecimal planned = (BigDecimal) categoryData.getOrDefault("planned", BigDecimal.ZERO);
            BigDecimal realized = (BigDecimal) categoryData.getOrDefault("realized", BigDecimal.ZERO);

            categoryData.put("planned", planned.add(line.getAmountPlanned()));
            categoryData.put("realized", realized.add(line.getAmountRealized()));

            byCategory.put(category, categoryData);
        }

        tracking.put("byCategory", byCategory);

        // Lignes avec le plus grand écart
        List<Map<String, Object>> topVariances = budget.getLines().stream()
            .sorted((l1, l2) -> l2.getVariance().abs().compareTo(l1.getVariance().abs()))
            .limit(5)
            .map(line -> {
                Map<String, Object> lineData = new HashMap<>();
                lineData.put("name", line.getName());
                lineData.put("category", line.getCategory());
                lineData.put("planned", line.getAmountPlanned());
                lineData.put("realized", line.getAmountRealized());
                lineData.put("variance", line.getVariance());
                lineData.put("isOverBudget", line.isOverBudget());
                return lineData;
            })
            .collect(Collectors.toList());

        tracking.put("topVariances", topVariances);

        return tracking;
    }

    /**
     * Calcule la date de début selon la période
     */
    private LocalDate calculateStartDate(String period, int year, Integer month) {
        return switch (period) {
            case "MONTHLY" -> LocalDate.of(year, month != null ? month : 1, 1);
            case "QUARTERLY" -> LocalDate.of(year, 1, 1); // Simplification: Q1
            case "SEMIANNUAL" -> LocalDate.of(year, 1, 1); // Simplification: S1
            case "ANNUAL" -> LocalDate.of(year, 1, 1);
            default -> LocalDate.of(year, 1, 1);
        };
    }

    /**
     * Calcule la date de fin selon la période
     */
    private LocalDate calculateEndDate(String period, int year, Integer month) {
        return switch (period) {
            case "MONTHLY" -> {
                int m = month != null ? month : 1;
                yield LocalDate.of(year, m, 1).plusMonths(1).minusDays(1);
            }
            case "QUARTERLY" -> LocalDate.of(year, 3, 31); // Simplification: Q1
            case "SEMIANNUAL" -> LocalDate.of(year, 6, 30); // Simplification: S1
            case "ANNUAL" -> LocalDate.of(year, 12, 31);
            default -> LocalDate.of(year, 12, 31);
        };
    }

    /**
     * Met à jour un budget
     */
    @Transactional
    public BudgetResponse updateBudget(UUID budgetId, Map<String, Object> updates) {
        LOG.infof("Mise à jour du budget %s", budgetId);

        Budget budget = budgetRepository.findByIdOptional(budgetId)
            .orElseThrow(() -> new NotFoundException("Budget non trouvé: " + budgetId));

        // Mise à jour des champs simples
        if (updates.containsKey("name")) {
            budget.setName((String) updates.get("name"));
        }
        if (updates.containsKey("description")) {
            budget.setDescription((String) updates.get("description"));
        }
        if (updates.containsKey("status")) {
            String newStatus = (String) updates.get("status");
            if (!List.of("DRAFT", "ACTIVE", "CLOSED", "ARCHIVED").contains(newStatus)) {
                throw new BadRequestException("Statut invalide: " + newStatus);
            }
            budget.setStatus(newStatus);

            // Si on active le budget, enregistrer la date d'approbation
            if ("ACTIVE".equals(newStatus) && budget.getApprovedAt() == null) {
                budget.setApprovedAt(LocalDateTime.now());
                budget.setApprovedById(UUID.fromString(jwt.getClaim("sub")));
            }
        }

        budgetRepository.persist(budget);
        LOG.infof("Budget %s mis à jour avec succès", budgetId);

        return toResponse(budget);
    }

    /**
     * Supprime un budget (soft delete)
     */
    @Transactional
    public void deleteBudget(UUID budgetId) {
        LOG.infof("Suppression du budget %s", budgetId);

        Budget budget = budgetRepository.findByIdOptional(budgetId)
            .orElseThrow(() -> new NotFoundException("Budget non trouvé: " + budgetId));

        // Soft delete: marquer comme inactif
        budget.setActif(false);
        budgetRepository.persist(budget);

        LOG.infof("Budget %s supprimé avec succès", budgetId);
    }

    /**
     * Convertit une entité Budget en DTO de réponse
     */
    private BudgetResponse toResponse(Budget budget) {
        List<BudgetLineResponse> linesResponse = budget.getLines().stream()
            .map(line -> BudgetLineResponse.builder()
                .id(line.getId())
                .category(line.getCategory())
                .name(line.getName())
                .description(line.getDescription())
                .amountPlanned(line.getAmountPlanned())
                .amountRealized(line.getAmountRealized())
                .notes(line.getNotes())
                // Champs calculés
                .realizationRate(line.getRealizationRate())
                .variance(line.getVariance())
                .isOverBudget(line.isOverBudget())
                .build())
            .collect(Collectors.toList());

        return BudgetResponse.builder()
            .id(budget.getId())
            .name(budget.getName())
            .description(budget.getDescription())
            .organizationId(budget.getOrganisation().getId())
            .period(budget.getPeriod())
            .year(budget.getYear())
            .month(budget.getMonth())
            .status(budget.getStatus())
            .lines(linesResponse)
            .totalPlanned(budget.getTotalPlanned())
            .totalRealized(budget.getTotalRealized())
            .currency(budget.getCurrency())
            .createdById(budget.getCreatedById())
            .createdAt(budget.getCreatedAtBudget())
            .approvedAt(budget.getApprovedAt())
            .approvedById(budget.getApprovedById())
            .startDate(budget.getStartDate())
            .endDate(budget.getEndDate())
            .metadata(budget.getMetadata())
            // Champs calculés
            .realizationRate(budget.getRealizationRate())
            .variance(budget.getVariance())
            .varianceRate(budget.getTotalPlanned().compareTo(BigDecimal.ZERO) > 0
                    ? budget.getVariance().doubleValue() / budget.getTotalPlanned().doubleValue() * 100
                    : 0.0)
            .isOverBudget(budget.isOverBudget())
            .isActive(budget.isActive())
            .isCurrentPeriod(budget.isCurrentPeriod())
            .build();
    }
}
