package pl.polskalokalnie.campaign;

import java.time.Instant;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import pl.polskalokalnie.lead.Lead;
import pl.polskalokalnie.lead.LeadQueryService;
import pl.polskalokalnie.lead.TargetGroup;
import pl.polskalokalnie.lead.TargetGroupRepository;

/**
 * Logika kampanii marketingowych: CRUD, budowa outboxu z grupy docelowej oraz sterowanie
 * wysylka (harmonogram/pauza). Outbox respektuje zgody marketingowe i trwale rezygnacje -
 * odbiorcy bez zgody lub po opt-oucie nie trafiaja do kolejki. Sama wysylka odbywa sie w workerze.
 */
@Service
public class CampaignService {

    private final CampaignRepository campaignRepository;
    private final CampaignRecipientRepository recipientRepository;
    private final TargetGroupRepository targetGroupRepository;
    private final LeadQueryService leadQueryService;

    public CampaignService(CampaignRepository campaignRepository,
            CampaignRecipientRepository recipientRepository,
            TargetGroupRepository targetGroupRepository,
            LeadQueryService leadQueryService) {
        this.campaignRepository = campaignRepository;
        this.recipientRepository = recipientRepository;
        this.targetGroupRepository = targetGroupRepository;
        this.leadQueryService = leadQueryService;
    }

    public List<CampaignResponse> list() {
        return campaignRepository.findAll().stream()
                .sorted(Comparator.comparing(Campaign::getCreatedAt).reversed())
                .map(this::toResponse)
                .toList();
    }

    public CampaignResponse get(Long id) {
        return toResponse(requireCampaign(id));
    }

    @Transactional
    public CampaignResponse create(CampaignRequest request, String createdBy) {
        requireGroup(request.targetGroupId());
        if (request.channel() == Channel.EMAIL && (request.subject() == null || request.subject().isBlank())) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Kampania e-mail wymaga tematu");
        }
        Campaign campaign = new Campaign();
        apply(campaign, request);
        campaign.setStatus(CampaignStatus.DRAFT);
        campaign.setCreatedBy(createdBy);
        return toResponse(campaignRepository.save(campaign));
    }

    @Transactional
    public CampaignResponse update(Long id, CampaignRequest request) {
        Campaign campaign = requireCampaign(id);
        if (campaign.getStatus() != CampaignStatus.DRAFT) {
            throw new ResponseStatusException(HttpStatus.CONFLICT, "Edytować można tylko kampanię w wersji roboczej");
        }
        requireGroup(request.targetGroupId());
        if (request.channel() == Channel.EMAIL && (request.subject() == null || request.subject().isBlank())) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Kampania e-mail wymaga tematu");
        }
        apply(campaign, request);
        return toResponse(campaignRepository.save(campaign));
    }

    @Transactional
    public void delete(Long id) {
        Campaign campaign = requireCampaign(id);
        recipientRepository.deleteAll(recipientRepository.findByCampaignId(campaign.getId()));
        campaignRepository.delete(campaign);
    }

    /**
     * Uruchamia kampanie: buduje outbox (jesli jeszcze nie istnieje) i ustawia status. Kampania
     * z data w przyszlosci przechodzi w SCHEDULED, w przeciwnym razie od razu w SENDING - workera
     * wysylajacego dodajemy w kolejnej fazie.
     */
    @Transactional
    public CampaignResponse schedule(Long id) {
        Campaign campaign = requireCampaign(id);
        if (campaign.getStatus() == CampaignStatus.COMPLETED || campaign.getStatus() == CampaignStatus.CANCELLED) {
            throw new ResponseStatusException(HttpStatus.CONFLICT, "Kampania jest już zakończona");
        }
        if (!recipientRepository.existsByCampaignId(campaign.getId())) {
            int built = buildOutbox(campaign);
            if (built == 0) {
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                        "Brak odbiorców spełniających kryteria (uwzględniono zgody i rezygnacje)");
            }
        }
        boolean future = campaign.getScheduledAt() != null && campaign.getScheduledAt().isAfter(Instant.now());
        campaign.setStatus(future ? CampaignStatus.SCHEDULED : CampaignStatus.SENDING);
        return toResponse(campaignRepository.save(campaign));
    }

    @Transactional
    public CampaignResponse pause(Long id) {
        Campaign campaign = requireCampaign(id);
        if (campaign.getStatus() != CampaignStatus.SENDING && campaign.getStatus() != CampaignStatus.SCHEDULED) {
            throw new ResponseStatusException(HttpStatus.CONFLICT, "Wstrzymać można tylko kampanię w toku lub zaplanowaną");
        }
        campaign.setStatus(CampaignStatus.PAUSED);
        return toResponse(campaignRepository.save(campaign));
    }

    public CampaignStatsResponse stats(Long id) {
        Campaign campaign = requireCampaign(id);
        Long cid = campaign.getId();
        return new CampaignStatsResponse(
                recipientRepository.findByCampaignId(cid).size(),
                recipientRepository.countByCampaignIdAndStatus(cid, RecipientStatus.QUEUED),
                recipientRepository.countByCampaignIdAndStatus(cid, RecipientStatus.SENT),
                recipientRepository.countByCampaignIdAndStatus(cid, RecipientStatus.DELIVERED),
                recipientRepository.countByCampaignIdAndStatus(cid, RecipientStatus.FAILED),
                recipientRepository.countByCampaignIdAndStatus(cid, RecipientStatus.BOUNCED),
                recipientRepository.countByCampaignIdAndStatus(cid, RecipientStatus.OPTED_OUT)
        );
    }

    // Buduje pozycje outboxu dla leadow z grupy, z poszanowaniem zgod, rezygnacji i obecnosci adresu.
    private int buildOutbox(Campaign campaign) {
        TargetGroup group = requireGroup(campaign.getTargetGroupId());
        List<Lead> members = leadQueryService.resolve(group);
        Set<String> seenAddresses = new LinkedHashSet<>();
        int built = 0;
        for (Lead lead : members) {
            String address = eligibleAddress(lead, campaign.getChannel());
            if (address == null) {
                continue;
            }
            if (!seenAddresses.add(address.toLowerCase(Locale.ROOT))) {
                continue;
            }
            CampaignRecipient recipient = new CampaignRecipient();
            recipient.setCampaignId(campaign.getId());
            recipient.setLeadId(lead.getId());
            recipient.setChannel(campaign.getChannel());
            recipient.setAddress(address);
            recipient.setName(lead.getName());
            recipient.setStatus(RecipientStatus.QUEUED);
            recipientRepository.save(recipient);
            built++;
        }
        return built;
    }

    // Zwraca adres kwalifikujacy leada do wysylki na danym kanale albo null, gdy nie kwalifikuje sie
    // (brak adresu, brak zgody marketingowej lub trwala rezygnacja).
    private static String eligibleAddress(Lead lead, Channel channel) {
        if (channel == Channel.EMAIL) {
            if (hasText(lead.getEmail()) && lead.isEmailConsent() && !lead.isEmailOptOut()) {
                return lead.getEmail().trim();
            }
            return null;
        }
        if (hasText(lead.getPhone()) && lead.isSmsConsent() && !lead.isSmsOptOut()) {
            return lead.getPhone().trim();
        }
        return null;
    }

    private void apply(Campaign campaign, CampaignRequest request) {
        campaign.setName(request.name().trim());
        campaign.setChannel(request.channel());
        campaign.setSubject(trimToNull(request.subject()));
        campaign.setBody(request.body());
        campaign.setTargetGroupId(request.targetGroupId());
        campaign.setScheduledAt(request.scheduledAt());
        campaign.setDailyLimit(request.dailyLimit());
    }

    private Campaign requireCampaign(Long id) {
        return campaignRepository.findById(id)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Kampania nie istnieje"));
    }

    private TargetGroup requireGroup(Long id) {
        return targetGroupRepository.findById(id)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Wskazana grupa docelowa nie istnieje"));
    }

    private CampaignResponse toResponse(Campaign campaign) {
        long total = recipientRepository.findByCampaignId(campaign.getId()).size();
        long sent = recipientRepository.countByCampaignIdAndStatus(campaign.getId(), RecipientStatus.SENT)
                + recipientRepository.countByCampaignIdAndStatus(campaign.getId(), RecipientStatus.DELIVERED);
        return CampaignResponse.from(campaign, total, sent);
    }

    private static boolean hasText(String value) {
        return value != null && !value.trim().isEmpty();
    }

    private static String trimToNull(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }
}
