package org.raddatz.familienarchiv.geschichte;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.owasp.html.HtmlPolicyBuilder;
import org.owasp.html.PolicyFactory;
import org.raddatz.familienarchiv.geschichte.GeschichteUpdateDTO;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.document.Document;
import org.raddatz.familienarchiv.geschichte.Geschichte;
import org.raddatz.familienarchiv.geschichte.GeschichteStatus;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.geschichte.GeschichteRepository;
import org.raddatz.familienarchiv.geschichte.GeschichteSpecifications;
import org.raddatz.familienarchiv.security.Permission;
import org.raddatz.familienarchiv.document.DocumentService;
import org.raddatz.familienarchiv.person.PersonService;
import org.raddatz.familienarchiv.user.UserService;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;

@Service
@RequiredArgsConstructor
@Slf4j
public class GeschichteService {

    private final GeschichteRepository geschichteRepository;
    private final PersonService personService;
    private final DocumentService documentService;
    private final UserService userService;

    /**
     * Allow-list policy for Geschichte body HTML. Tiptap on the writer side
     * already constrains the marks/nodes, but the backend re-sanitises every
     * save so that an attacker calling the API directly cannot inject more.
     */
    private static final PolicyFactory BODY_SANITIZER = new HtmlPolicyBuilder()
            .allowElements("p", "br", "strong", "em", "h2", "h3", "ul", "ol", "li")
            .toFactory();

    private static final int DEFAULT_LIMIT = 50;
    private static final int MAX_LIMIT = 200;

    // ─── Read API ────────────────────────────────────────────────────────────

    public Geschichte getById(UUID id) {
        Geschichte g = geschichteRepository.findById(id)
                .orElseThrow(() -> DomainException.notFound(
                        ErrorCode.GESCHICHTE_NOT_FOUND, "Geschichte not found: " + id));
        if (g.getStatus() == GeschichteStatus.DRAFT && !currentUserHasBlogWrite()) {
            // Use NOT_FOUND, not FORBIDDEN — don't leak DRAFT existence.
            throw DomainException.notFound(
                    ErrorCode.GESCHICHTE_NOT_FOUND, "Geschichte not found: " + id);
        }
        return g;
    }

    /**
     * Lists Geschichten with optional filters. {@code personIds} uses AND semantics: the story
     * must be associated with every person id supplied. An empty or null list applies no
     * person filter. Result is ordered by {@code COALESCE(publishedAt, updatedAt) DESC}.
     */
    public List<Geschichte> list(GeschichteStatus status, List<UUID> personIds, UUID documentId, int limit) {
        GeschichteStatus effective = currentUserHasBlogWrite() ? status : GeschichteStatus.PUBLISHED;
        int safeLimit = limit <= 0 ? DEFAULT_LIMIT : Math.min(limit, MAX_LIMIT);

        Specification<Geschichte> spec = Specification.allOf(
                GeschichteSpecifications.hasStatus(effective),
                GeschichteSpecifications.hasAllPersons(personIds),
                GeschichteSpecifications.hasDocument(documentId),
                GeschichteSpecifications.orderByDisplayDateDesc()
        );
        return geschichteRepository.findAll(spec, Sort.unsorted())
                .stream()
                .limit(safeLimit)
                .toList();
    }

    // ─── Write API ───────────────────────────────────────────────────────────

    @Transactional
    public Geschichte create(GeschichteUpdateDTO dto) {
        requireTitle(dto.getTitle());
        Geschichte g = Geschichte.builder()
                .title(dto.getTitle().trim())
                .body(sanitize(dto.getBody()))
                .status(GeschichteStatus.DRAFT)
                .author(currentUser())
                .persons(resolvePersons(dto.getPersonIds()))
                .documents(resolveDocuments(dto.getDocumentIds()))
                .build();
        if (dto.getStatus() == GeschichteStatus.PUBLISHED) {
            g.setStatus(GeschichteStatus.PUBLISHED);
            g.setPublishedAt(LocalDateTime.now());
        }
        return geschichteRepository.save(g);
    }

    @Transactional
    public Geschichte update(UUID id, GeschichteUpdateDTO dto) {
        Geschichte g = geschichteRepository.findById(id)
                .orElseThrow(() -> DomainException.notFound(
                        ErrorCode.GESCHICHTE_NOT_FOUND, "Geschichte not found: " + id));
        if (dto.getTitle() != null) {
            requireTitle(dto.getTitle());
            g.setTitle(dto.getTitle().trim());
        }
        if (dto.getBody() != null) {
            g.setBody(sanitize(dto.getBody()));
        }
        if (dto.getPersonIds() != null) {
            g.setPersons(resolvePersons(dto.getPersonIds()));
        }
        if (dto.getDocumentIds() != null) {
            g.setDocuments(resolveDocuments(dto.getDocumentIds()));
        }
        if (dto.getStatus() != null && dto.getStatus() != g.getStatus()) {
            applyStatusTransition(g, dto.getStatus());
        }
        return geschichteRepository.save(g);
    }

    @Transactional
    public void delete(UUID id) {
        if (!geschichteRepository.existsById(id)) {
            throw DomainException.notFound(
                    ErrorCode.GESCHICHTE_NOT_FOUND, "Geschichte not found: " + id);
        }
        geschichteRepository.deleteById(id);
    }

    // ─── private helpers ─────────────────────────────────────────────────────

    private void applyStatusTransition(Geschichte g, GeschichteStatus next) {
        g.setStatus(next);
        if (next == GeschichteStatus.PUBLISHED) {
            g.setPublishedAt(LocalDateTime.now());
        } else {
            g.setPublishedAt(null);
        }
    }

    private void requireTitle(String title) {
        if (title == null || title.trim().isEmpty()) {
            throw DomainException.badRequest(
                    ErrorCode.VALIDATION_ERROR, "Title is required");
        }
    }

    private String sanitize(String body) {
        if (body == null) return null;
        return BODY_SANITIZER.sanitize(body);
    }

    private Set<Person> resolvePersons(List<UUID> ids) {
        if (ids == null || ids.isEmpty()) return new HashSet<>();
        return new LinkedHashSet<>(personService.getAllById(ids));
    }

    private Set<Document> resolveDocuments(List<UUID> ids) {
        if (ids == null || ids.isEmpty()) return new HashSet<>();
        Set<Document> out = new LinkedHashSet<>();
        for (UUID id : ids) {
            out.add(documentService.getDocumentById(id));
        }
        return out;
    }

    private AppUser currentUser() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth == null || !auth.isAuthenticated()) {
            throw DomainException.unauthorized("Authentication required");
        }
        return userService.findByEmail(auth.getName());
    }

    private boolean currentUserHasBlogWrite() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth == null || !auth.isAuthenticated()) return false;
        return auth.getAuthorities().stream()
                .anyMatch(a -> Permission.BLOG_WRITE.name().equals(a.getAuthority()));
    }
}
