package cloud.nstart.terravault.ordinis.restapi.web;

import cloud.nstart.terravault.ordinis.domain.notification.NotificationLog;
import cloud.nstart.terravault.ordinis.domain.notification.NotificationLogRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.HttpStatus;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Me-scoped notifications endpoints — current user sees their own notification log.
 *
 * <p>Pagination is cursor-less for simplicity (Phase A). The recipient column in
 * notification_log stores the email address, so we look up current user's email
 * from the JWT {@code email} claim as the lookup key.
 *
 * <p>Read state is tracked in {@code notification_read_state} table (migration 0024)
 * — a separate table keyed by (user_id, notification_id) so the log itself stays
 * append-only with no mutable state.
 *
 * <p>The {@code totalUnread} count is computed as notifications without a read_state
 * row for the current user.
 */
@RestController
@RequestMapping("/api/v1/me/notifications")
public class MeNotificationsController {

  private static final Logger log = LoggerFactory.getLogger(MeNotificationsController.class);
  private static final int MAX_LIMIT = 100;

  private final NotificationLogRepository notificationLogRepository;
  private final JdbcTemplate jdbcTemplate;

  public MeNotificationsController(
      NotificationLogRepository notificationLogRepository,
      JdbcTemplate jdbcTemplate) {
    this.notificationLogRepository = notificationLogRepository;
    this.jdbcTemplate = jdbcTemplate;
  }

  /**
   * List notifications for the current user.
   *
   * <p>Query param {@code status} filters by notification_log.status (e.g. SENT).
   * If omitted, returns all statuses. {@code limit} caps at 100.
   *
   * @return paginated response with items and totalUnread count
   */
  @GetMapping
  public Map<String, Object> list(
      @RequestParam(required = false) String status,
      @RequestParam(defaultValue = "20") int limit) {
    // Panic-level defensiveness: this endpoint is hit by the bell badge
    // poll on EVERY page (every 30s). A 500 here breaks global UX, even
    // for users who don't care about notifications.
    //
    // Strategy: wrap the entire handler in try/catch. Anonymous request
    // OR unauthenticated context returns empty cleanly (no 403 throw —
    // bell на logged-out landing not worth a guard violation). Any other
    // failure also returns empty + logs the cause for follow-up.
    try {
      String sub = currentUserSubSafe();
      if (sub == null) {
        // Anonymous / no JWT → no notifications possible. Don't throw 403,
        // just say "no notifications" — semantically true.
        return emptyResponse();
      }

      String emailKey = currentUserEmailSafe();
      if (emailKey == null || emailKey.isBlank()) {
        // Authenticated but no email claim. Recipient lookup uses email,
        // so without it we have nothing to fetch.
        log.debug("No email claim in JWT for user sub={}", sub);
        return emptyResponse();
      }

      int safeLimit = Math.min(Math.max(limit, 1), MAX_LIMIT);

      List<NotificationItemDto> items;
      try {
        Page<NotificationLog> page = notificationLogRepository.findByRecipientOrderBySentAtDesc(
            emailKey, PageRequest.of(0, safeLimit));
        items = page.getContent().stream()
            .filter(n -> status == null || (n.getStatus() != null && status.equalsIgnoreCase(n.getStatus().name())))
            .map(n -> {
              try {
                return toDto(n, sub);
              } catch (RuntimeException e) {
                log.warn("Failed to map NotificationLog id={} to DTO: {}", n.getId(), e.getMessage());
                return null;
              }
            })
            .filter(java.util.Objects::nonNull)
            .toList();
      } catch (RuntimeException e) {
        log.error("Failed to load notifications for user sub={}, email={}: {}",
            sub, emailKey, e.getMessage(), e);
        items = List.of();
      }

      long totalUnread = 0L;
      try {
        totalUnread = countUnread(sub, emailKey);
      } catch (RuntimeException e) {
        log.error("Failed to count unread notifications for user sub={}: {}",
            sub, e.getMessage(), e);
      }

      Map<String, Object> response = new HashMap<>();
      response.put("items", items);
      response.put("totalUnread", totalUnread);
      return response;
    } catch (RuntimeException e) {
      // Last-resort catch: anything we missed. Don't propagate to the
      // global handler (which would 500). Log and return empty.
      log.error("Unexpected error in GET /me/notifications", e);
      return emptyResponse();
    }
  }

  private static Map<String, Object> emptyResponse() {
    Map<String, Object> r = new HashMap<>();
    r.put("items", List.of());
    r.put("totalUnread", 0L);
    return r;
  }

  /** Returns sub claim from JWT or null if missing/anonymous. Never throws. */
  private String currentUserSubSafe() {
    try {
      return currentUserSub();
    } catch (RuntimeException e) {
      log.warn("Failed to read JWT sub claim: {}", e.getMessage());
      return null;
    }
  }

  /** Returns email claim from JWT or null. Never throws. */
  private String currentUserEmailSafe() {
    try {
      return currentUserEmail();
    } catch (RuntimeException e) {
      log.warn("Failed to read JWT email claim: {}", e.getMessage());
      return null;
    }
  }

  /**
   * Mark a single notification as read.
   */
  @PostMapping("/{id}/read")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  public void markRead(@PathVariable Long id) {
    String sub = requireCurrentUserSub();
    markReadState(sub, id);
  }

  /**
   * Mark all notifications for current user as read.
   */
  @PostMapping("/read-all")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  public void markAllRead() {
    String sub = requireCurrentUserSub();
    String emailKey = currentUserEmail();
    if (emailKey == null || emailKey.isBlank()) return;

    // Find all unread notification ids for this recipient and mark them read
    Page<NotificationLog> page = notificationLogRepository.findByRecipientOrderBySentAtDesc(
        emailKey, PageRequest.of(0, MAX_LIMIT));

    page.getContent().forEach(n -> markReadState(sub, n.getId()));
    log.debug("Marked {} notifications read for user {}", page.getNumberOfElements(), sub);
  }

  // --- internals ---

  private void markReadState(String sub, Long notificationId) {
    jdbcTemplate.update("""
        INSERT INTO notification_read_state (user_id, notification_id, read_at)
        VALUES (?, ?, ?)
        ON CONFLICT (user_id, notification_id) DO NOTHING
        """, sub, notificationId, OffsetDateTime.now());
  }

  private long countUnread(String sub, String emailKey) {
    Long count = jdbcTemplate.queryForObject("""
        SELECT COUNT(*) FROM notification_log nl
        WHERE nl.recipient = ?
          AND nl.status = 'SENT'
          AND NOT EXISTS (
            SELECT 1 FROM notification_read_state rs
            WHERE rs.user_id = ? AND rs.notification_id = nl.id
          )
        """, Long.class, emailKey, sub);
    return count != null ? count : 0L;
  }

  private NotificationItemDto toDto(NotificationLog n, String sub) {
    boolean read = isRead(sub, n.getId());
    String preview = buildPreview(n);
    return new NotificationItemDto(
        n.getId(),
        n.getEventType(),
        n.getChannel(),
        n.getSentAt(),
        preview,
        read
    );
  }

  private boolean isRead(String sub, Long notificationId) {
    // PostgreSQL COUNT(*) returns BIGINT — request Long.class to avoid
    // narrowing-conversion edge cases в Spring's RowMapper. Was Integer.class
    // which Spring auto-converts via rs.getInt() (works but type-imprecise).
    Long count = jdbcTemplate.queryForObject(
        "SELECT COUNT(*) FROM notification_read_state WHERE user_id = ? AND notification_id = ?",
        Long.class, sub, notificationId);
    return count != null && count > 0L;
  }

  private static String buildPreview(NotificationLog n) {
    // Construct a human-readable preview from event_type + event metadata
    return switch (n.getEventType()) {
      case "RecordDraftApproved" -> "Draft approved";
      case "RecordDraftRejected" -> "Draft rejected";
      case "RecordDraftWithdrawn" -> "Draft withdrawn";
      case "RecordDraftSubmitted" -> "New draft submitted for review";
      default -> n.getEventType();
    };
  }

  private String requireCurrentUserSub() {
    String sub = currentUserSub();
    if (sub == null) {
      throw cloud.nstart.terravault.ordinis.restapi.error.OrdinisException.forbidden(
          "unauthenticated", "Authentication required for /me/notifications");
    }
    return sub;
  }

  private String currentUserSub() {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    if (!(auth instanceof JwtAuthenticationToken jwt)) return null;
    return jwt.getToken().getSubject();
  }

  private String currentUserEmail() {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    if (!(auth instanceof JwtAuthenticationToken jwt)) return null;
    Jwt token = jwt.getToken();
    return token.getClaimAsString("email");
  }

  /**
   * Response shape for a single notification item.
   */
  public record NotificationItemDto(
      Long id,
      String eventType,
      String channel,
      OffsetDateTime sentAt,
      String payloadPreview,
      boolean read
  ) {}
}
