package com.example.ldp.service;

import java.time.Instant;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.ldp.api.dto.DanceAdminRowDto;
import com.example.ldp.api.dto.DanceAdminRowDto.ChoreographerSummary;
import com.example.ldp.api.dto.DanceAdminRowDto.PrimarySongSummary;
import com.example.ldp.api.dto.DeleteDancePreviewDto;
import com.example.ldp.api.dto.PageResponse;
import com.example.ldp.config.ErrorCode;
import com.example.ldp.domain.Choreographer;
import com.example.ldp.domain.Dance;
import com.example.ldp.domain.DanceListingStatus;
import com.example.ldp.domain.DanceSong;
import com.example.ldp.domain.Link;
import com.example.ldp.domain.Song;
import com.example.ldp.domain.enums.DanceSongRole;
import com.example.ldp.repo.ChannelVideoRepository;
import com.example.ldp.repo.DanceMergeHistoryRepository;
import com.example.ldp.repo.DanceRepository;
import com.example.ldp.repo.DanceSongRepository;
import com.example.ldp.repo.DanceSuggestionRepository;
import com.example.ldp.repo.EventSetlistItemRepository;
import com.example.ldp.repo.LineDanceSpecRepository;
import com.example.ldp.repo.LinkRepository;
import com.example.ldp.repo.SongRepository;
import com.example.ldp.repo.spec.DanceSpecs;
import com.example.ldp.service.exception.BadRequestException;
import com.example.ldp.service.exception.ClaimException;
import com.example.ldp.service.exception.ConflictException;
import com.example.ldp.service.exception.NotFoundException;
import com.example.ldp.user.User;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * Service for admin-only dance operations.
 */
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class AdminDanceService {

    private final DanceRepository danceRepository;
    private final LinkRepository linkRepository;
    private final LineDanceSpecRepository lineDanceSpecRepository;
    private final DanceSongRepository danceSongRepository;
    private final SongRepository songRepository;
    private final EventSetlistItemRepository eventSetlistItemRepository;
    private final DanceMergeHistoryRepository danceMergeHistoryRepository;
    private final DanceSuggestionRepository danceSuggestionRepository;
    private final ChannelVideoRepository channelVideoRepository;

    /**
     * Whitelist de camps ordenables (nom públic → propietat JPA). Bloqueja
     * Sort per propietats arbitràries (PropertyReferenceException) i
     * documenta el contracte amb el frontend.
     */
    private static final Map<String, String> SORTABLE_FIELDS = Map.of(
            "id", "id",
            "name", "name",
            "level", "level.code",
            "counts", "counts",
            "walls", "walls",
            "originYear", "originYear"
    );
    private static final String DEFAULT_SORT = "name";

    /**
     * List all dances with optional filtering and pagination for admin
     * management. Fetches all related data efficiently to avoid N+1.
     *
     * @param page 0-indexed page number
     * @param size page size (already capped by controller)
     * @return PageResponse with dances and pagination metadata
     */
    public PageResponse<DanceAdminRowDto> listDances(
            String q,
            Long id,
            String level,
            Integer counts,
            Integer walls,
            Integer yearFrom,
            Integer yearTo,
            Boolean hasPrimarySong,
            String listingStatus,
            Boolean hasSpec,
            Boolean hasPrimaryVideo,
            Boolean unknownYear,
            Boolean unknownCounts,
            Boolean hasChoreographer,
            String choreographerCountry,
            String sortBy,
            String sortDir,
            int page,
            int size
    ) {
        // 1. Compose filters as a JPA Specification — DB does the work.
        Specification<Dance> spec = Specification
                .where(DanceSpecs.idEquals(id))
                .and(DanceSpecs.nameOrPrimarySongContains(q))
                .and(DanceSpecs.levelEquals(level))
                .and(DanceSpecs.countsEquals(counts))
                .and(DanceSpecs.wallsEquals(walls))
                .and(DanceSpecs.yearBetween(yearFrom, yearTo))
                .and(DanceSpecs.listingStatusEquals(listingStatus))
                .and(DanceSpecs.unknownYear(unknownYear))
                .and(DanceSpecs.unknownCounts(unknownCounts))
                .and(DanceSpecs.hasPrimarySong(hasPrimarySong))
                .and(DanceSpecs.hasSpec(hasSpec))
                .and(DanceSpecs.hasPrimaryVideo(hasPrimaryVideo))
                .and(DanceSpecs.hasChoreographer(hasChoreographer))
                .and(DanceSpecs.choreographerCountryContains(choreographerCountry));

        // 2. Build pageable with whitelisted Sort. ORDER BY + LIMIT pushed to the DB.
        Pageable pageable = PageRequest.of(page, size, buildSort(sortBy, sortDir));
        Page<Dance> idPage = danceRepository.findAll(spec, pageable);

        List<Long> pagedIds = idPage.getContent().stream().map(Dance::getId).toList();
        if (pagedIds.isEmpty()) {
            return new PageResponse<>(List.of(), page, size, idPage.getTotalElements(), idPage.getTotalPages());
        }

        // 3. Batch-load associations for the page only — no N+1, no JOIN FETCH+Pageable trap.
        Map<Long, Dance> byId = danceRepository.findWithDetailsByIdIn(pagedIds).stream()
                .collect(Collectors.toMap(Dance::getId, d -> d));
        List<Dance> ordered = pagedIds.stream().map(byId::get).filter(Objects::nonNull).toList();

        // 4. Reuse existing batch-load helpers, scoped to the page.
        Set<Long> danceIdsWithSpec = new HashSet<>(lineDanceSpecRepository.findDanceIdsByDanceIdIn(pagedIds));
        Map<Long, String> primaryVideoIdByDanceId = linkRepository.findPrimaryByDanceIds(pagedIds).stream()
                .collect(Collectors.toMap(
                        link -> link.getDance().getId(),
                        Link::getVideoId,
                        (v1, v2) -> v1
                ));

        List<DanceAdminRowDto> content = ordered.stream()
                .map(d -> toAdminRowDto(d, primaryVideoIdByDanceId, danceIdsWithSpec))
                .toList();

        return new PageResponse<>(content, page, size, idPage.getTotalElements(), idPage.getTotalPages());
    }

    /**
     * Construeix un Sort validat contra la whitelist {@link #SORTABLE_FIELDS}.
     * Camps numèrics nullables (counts, walls, originYear) van amb NULLS LAST
     * per preservar la semàntica del comparator anterior; afegim un tie-breaker
     * estable per ID per evitar salts de pàgina entre requests.
     */
    private Sort buildSort(String sortBy, String sortDir) {
        String property = SORTABLE_FIELDS.getOrDefault(sortBy, SORTABLE_FIELDS.get(DEFAULT_SORT));
        Sort.Direction direction = "desc".equalsIgnoreCase(sortDir) ? Sort.Direction.DESC : Sort.Direction.ASC;
        Sort.Order primary = new Sort.Order(direction, property).nullsLast();
        if ("id".equals(property)) {
            return Sort.by(primary);
        }
        return Sort.by(primary).and(Sort.by(Sort.Direction.ASC, "id"));
    }

    /**
     * Canvia l'estat de publicació d'un ball. Transicions vàlides:
     * DRAFT→REVIEW→PUBLISHED, PUBLISHED→ARCHIVED, qualsevol→DRAFT
     */
    @Transactional
    public void updateListingStatus(Long danceId, String newStatusStr) {
        if (newStatusStr == null || newStatusStr.isBlank()) {
            throw new BadRequestException("listingStatus is required");
        }

        DanceListingStatus newStatus;
        try {
            newStatus = DanceListingStatus.valueOf(newStatusStr.trim().toUpperCase());
        } catch (IllegalArgumentException e) {
            throw new BadRequestException(
                    "Invalid listing status: " + newStatusStr
                    + ". Valid values: DRAFT, REVIEW, PUBLISHED, ARCHIVED");
        }

        Dance dance = danceRepository.findById(danceId)
                .orElseThrow(() -> new NotFoundException("Dance not found: " + danceId));

        // Registra la PRIMERA publicació; es preserva en re-publicacions posteriors.
        if (newStatus == DanceListingStatus.PUBLISHED && dance.getPublishedAt() == null) {
            dance.setPublishedAt(Instant.now());
        }

        dance.setListingStatus(newStatus);
        danceRepository.save(dance);
    }

    private DanceAdminRowDto toAdminRowDto(Dance d, Map<Long, String> primaryVideoIdByDanceId, Set<Long> danceIdsWithSpec) {
        // Find PRIMARY song
        PrimarySongSummary primarySong = d.getDanceSongs().stream()
                .filter(ds -> ds.getRole() == DanceSongRole.PRIMARY)
                .findFirst()
                .map(DanceSong::getSong)
                .map(this::toPrimarySongSummary)
                .orElse(null);

        // Map choreographers
        List<ChoreographerSummary> choreographers = d.getChoreographers().stream()
                .map(this::toChoreographerSummary)
                .collect(Collectors.toList());

        // Get PRIMARY video link from preloaded map (avoids N+1)
        String primaryVideoId = primaryVideoIdByDanceId.get(d.getId());

        boolean hasSpec = danceIdsWithSpec.contains(d.getId());
        boolean hasPrimaryChoreographer = !choreographers.isEmpty();

        return new DanceAdminRowDto(
                d.getId(),
                d.getName(),
                d.getLevel() != null ? d.getLevel().getCode() : null,
                d.getCounts(),
                d.getWalls(),
                d.getOriginYear(),
                primarySong,
                choreographers,
                primaryVideoId,
                d.getListingStatus() != null ? d.getListingStatus().name() : null,
                hasSpec,
                hasPrimaryChoreographer,
                d.isOriginYearIsEstimated()
        );
    }

    private PrimarySongSummary toPrimarySongSummary(Song s) {
        return new PrimarySongSummary(
                s.getId(),
                s.getTitle(),
                s.getArtist(),
                s.getBpm(),
                s.getYear()
        );
    }

    private ChoreographerSummary toChoreographerSummary(Choreographer c) {
        return new ChoreographerSummary(
                c.getId(),
                c.getName(),
                c.getCountry()
        );
    }

    /**
     * Links a song to a dance as PRIMARY. Fails if the dance already has a
     * PRIMARY song (use the full dance edit to replace it).
     */
    @Transactional
    public void linkSongToDance(Long danceId, Long songId) {
        Dance dance = danceRepository.findById(danceId)
                .orElseThrow(() -> new NotFoundException("Dance not found: " + danceId));

        Song song = songRepository.findById(songId)
                .orElseThrow(() -> new NotFoundException("Song not found: " + songId));

        boolean alreadyHasPrimary = dance.getDanceSongs().stream()
                .anyMatch(ds -> ds.getRole() == DanceSongRole.PRIMARY);
        if (alreadyHasPrimary) {
            throw new ConflictException("Dance already has a PRIMARY song. Use the edit page to replace it.");
        }

        if (danceSongRepository.existsByDanceIdAndSongId(danceId, songId)) {
            throw new ConflictException("This song is already linked to this dance.");
        }

        DanceSong danceSong = DanceSong.builder()
                .dance(dance)
                .song(song)
                .role(DanceSongRole.PRIMARY)
                .sortOrder(1)
                .build();
        danceSongRepository.save(danceSong);
    }

    /**
     * Get dances by choreographer ID with pagination. Used for the public
     * choreographer detail page.
     *
     * @param choreographerId choreographer ID
     * @param page 0-indexed page number
     * @param size page size
     * @return PageResponse with dances for this choreographer
     */
    public PageResponse<DanceAdminRowDto> getDancesByChoreographer(Long choreographerId, int page, int size) {
        List<Dance> allDances = danceRepository.findByChoreographerId(choreographerId);

        // Calculate pagination metadata
        long totalElements = allDances.size();
        int totalPages = (int) Math.ceil((double) totalElements / size);

        // Apply pagination manually
        int fromIndex = page * size;
        int toIndex = Math.min(fromIndex + size, allDances.size());

        List<Dance> pagedDances = fromIndex >= allDances.size()
                ? List.of()
                : allDances.subList(fromIndex, toIndex);

        // Preload PRIMARY links only for paged dances
        List<Long> danceIds = pagedDances.stream().map(Dance::getId).collect(Collectors.toList());
        Map<Long, String> primaryVideoIdByDanceId = danceIds.isEmpty() ? Map.of()
                : linkRepository.findPrimaryByDanceIds(danceIds).stream()
                        .collect(Collectors.toMap(
                                link -> link.getDance().getId(),
                                Link::getVideoId,
                                (v1, v2) -> v1
                        ));

        Set<Long> choreoDanceIdsWithSpec = new HashSet<>(lineDanceSpecRepository.findAllDanceIds());
        List<DanceAdminRowDto> content = pagedDances.stream()
                .map(d -> toAdminRowDto(d, primaryVideoIdByDanceId, choreoDanceIdsWithSpec))
                .collect(Collectors.toList());

        return new PageResponse<>(content, page, size, totalElements, totalPages);
    }

    // ==================== DELETE DANCE ====================

    /**
     * Pre-flight summary for hard-deleting a dance. Returns the cascade
     * footprint and any blockers (events using the dance, merge history rows
     * where this dance is the survivor). The admin can use this to decide
     * whether to delete or archive.
     */
    public DeleteDancePreviewDto previewDelete(Long id) {
        Dance dance = danceRepository.findById(id)
                .orElseThrow(() -> new NotFoundException("Dance not found: " + id));

        List<DeleteDancePreviewDto.DeleteBlocker> blockers = new java.util.ArrayList<>();

        List<Long> blockingEventIds = eventSetlistItemRepository.findDistinctEventIdsByDanceId(id);
        if (!blockingEventIds.isEmpty()) {
            blockers.add(new DeleteDancePreviewDto.DeleteBlocker(
                    "EVENT_SETLIST",
                    "El ball apareix al setlist de " + blockingEventIds.size() + " event(s).",
                    blockingEventIds
            ));
        }

        long survivorMergeCount = danceMergeHistoryRepository.countBySurvivorId(id);
        if (survivorMergeCount > 0) {
            blockers.add(new DeleteDancePreviewDto.DeleteBlocker(
                    "MERGE_SURVIVOR",
                    "Aquest ball és el survivor de " + survivorMergeCount
                            + " fusió(ns); eliminar-lo trencaria l'historial.",
                    List.of()
            ));
        }

        DeleteDancePreviewDto.CascadeSummary summary = new DeleteDancePreviewDto.CascadeSummary(
                danceSongRepository.countByDanceId(id),
                dance.getChoreographers() == null ? 0L : dance.getChoreographers().size(),
                linkRepository.countByDance_Id(id),
                lineDanceSpecRepository.existsByDanceId(id),
                channelVideoRepository.countByDance_Id(id),
                danceSuggestionRepository.countByCreatedDance_Id(id),
                0L, // merge history rows where dance is the loser → SET NULL; not currently counted (low value, optional)
                linkRepository.countOrphanLinksAfterDanceDelete(id)
        );

        return new DeleteDancePreviewDto(id, dance.getName(), blockers, summary);
    }

    /**
     * Hard-delete a dance with full safeguards.
     * <ol>
     *   <li>Refuses if any blocker is present (events using it, merge survivor).</li>
     *   <li>Requires the admin to type the exact dance name to confirm.</li>
     *   <li>Cleans up orphan links (decision D: links that lose their only
     *       attachment when dance_song rows are CASCADE-deleted).</li>
     * </ol>
     * The actual delete relies on DB-level CASCADE / SET NULL constraints
     * (V1, V46, V71, V72, V75, V77, V86).
     */
    @Transactional
    public void deleteDance(Long id, String confirmationName, User admin) {
        Dance dance = danceRepository.findById(id)
                .orElseThrow(() -> new NotFoundException("Dance not found: " + id));

        if (confirmationName == null || !confirmationName.trim().equals(dance.getName())) {
            throw new ClaimException(
                    "El nom escrit per confirmar no coincideix amb el nom del ball.",
                    ErrorCode.DANCE_DELETE_CONFIRMATION_MISMATCH);
        }

        DeleteDancePreviewDto preview = previewDelete(id);
        if (!preview.canDelete()) {
            String reasons = preview.blockers().stream()
                    .map(DeleteDancePreviewDto.DeleteBlocker::message)
                    .collect(Collectors.joining(" "));
            throw new ClaimException(
                    "No es pot eliminar el ball: " + reasons,
                    ErrorCode.DANCE_IN_USE);
        }

        // Decision D: pre-collect orphan link IDs before CASCADE clears
        // their dance_song_id (which would make the predicate moot).
        List<Long> orphanLinkIds = linkRepository.findOrphanLinkIdsAfterDanceDelete(id);

        log.info("ADMIN_DELETE_DANCE id={} name='{}' by admin={} cascadeSummary={} orphanLinks={}",
                id, dance.getName(), admin.getEmail(), preview.willCascade(), orphanLinkIds.size());

        danceRepository.delete(dance);
        danceRepository.flush(); // force CASCADE/SET NULL before orphan cleanup

        if (!orphanLinkIds.isEmpty()) {
            linkRepository.deleteAllById(orphanLinkIds);
        }
    }
}
