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

import cloud.nstart.terravault.ordinis.auth.ScopeContext;
import cloud.nstart.terravault.ordinis.domain.DataScope;
import cloud.nstart.terravault.ordinis.domain.audit.AuditLog;
import cloud.nstart.terravault.ordinis.domain.audit.AuditLogRepository;
import cloud.nstart.terravault.ordinis.domain.definition.DictionaryDefinition;
import cloud.nstart.terravault.ordinis.domain.record.DictionaryRecord;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.time.OffsetDateTime;
import java.util.Set;

/**
 * Запись в {@code audit_log} — юридически значимый лог. Источник истины для
 * compliance, отдельно от технических логов в Loki.
 *
 * <p>Захватывает: user (sub claim из JWT либо "anonymous"), scope, IP, UA,
 * trace_id из MDC (Micrometer Tracing). Вызывается из {@link
 * cloud.nstart.terravault.ordinis.restapi.service.DictionaryRecordService} в
 * той же транзакции что и change — at-least-once семантика.
 */
@Service
public class AuditLogger {

  private static final Logger log = LoggerFactory.getLogger(AuditLogger.class);

  private final AuditLogRepository repository;
  private final ScopeContext scopeContext;

  public AuditLogger(AuditLogRepository repository, ScopeContext scopeContext) {
    this.repository = repository;
    this.scopeContext = scopeContext;
  }

  public void recordCreate(DictionaryDefinition d, DictionaryRecord r) {
    write("RECORD_CREATED", "CREATE", d, r, null, r.getData());
  }

  /**
   * Audit для создания справочника. record-связанные поля null'ятся.
   *
   * <p>Action = {@code DEFINITION_CREATE} (не {@code CREATE}) — иначе
   * существующие фильтры по action=CREATE подхватывают dict-level row
   * без businessKey (SmokeE2ETest.contextLoadsAndCreatesRecord фейлится).
   */
  public void definitionCreated(DictionaryDefinition d) {
    write("DEFINITION_CREATED", "DEFINITION_CREATE", d, null, null, d.getSchemaJson());
  }

  /**
   * Audit для обновления schema справочника. payload_before/after —
   * schema до/после. Используется changelog для diff computation.
   */
  public void definitionUpdated(DictionaryDefinition d, JsonNode prevSchema) {
    write("DEFINITION_UPDATED", "DEFINITION_UPDATE", d, null, prevSchema, d.getSchemaJson());
  }

  public void recordUpdate(
      DictionaryDefinition d, DictionaryRecord r, JsonNode payloadBefore) {
    write("RECORD_UPDATED", "UPDATE", d, r, payloadBefore, r.getData());
  }

  public void recordClose(DictionaryDefinition d, DictionaryRecord r, String reason) {
    write("RECORD_CLOSED", "CLOSE", d, r, r.getData(), null);
  }

  /**
   * Schema workflow audit events (Phase 1 v2.2.0). Schema draft lifecycle
   * пишется в audit_log дополнительно к outbox чтобы ChangelogService
   * мог построить полный timeline (definition_* + schema_draft_*).
   *
   * <p>{@code before} = previous schema, {@code after} = proposed schema —
   * полезно для diff render'а в History tab UI: «draft предлагал +altitude
   * / ~operator» без необходимости отдельно fetch'ить proposed_schema.
   *
   * @param eventType e.g. {@code SCHEMA_DRAFT_CREATED}, {@code SCHEMA_DRAFT_PUBLISHED}
   * @param action e.g. {@code DRAFT_CREATE}, {@code DRAFT_PUBLISH}
   * @param d живой dict (для scope + id)
   * @param before previous schema (live HEAD на момент event'а)
   * @param after proposed schema (для published — final live schema)
   */
  public void schemaDraftEvent(
      String eventType, String action, DictionaryDefinition d,
      JsonNode before, JsonNode after) {
    write(eventType, action, d, null, before, after);
  }

  /**
   * Record-draft workflow audit events. Mirrors {@link #schemaDraftEvent} but
   * for record-level drafts (DraftService — submit / approve / reject /
   * withdraw). Closes the audit gap surfaced by the 2026-05-14 approval
   * workflow audit: until now record-draft decisions left no trail beyond
   * downstream RECORD_CREATE/UPDATE/CLOSE events emitted by
   * DictionaryRecordService.applyApproved*. Compliance needs the
   * decision-level event (who approved, when, why) explicitly.
   *
   * @param eventType e.g. {@code RECORD_DRAFT_SUBMITTED}, {@code RECORD_DRAFT_APPROVED}
   * @param action e.g. {@code DRAFT_SUBMIT}, {@code DRAFT_APPROVE}
   * @param d живой dict (для scope + id)
   * @param businessKey записи которой касается draft (null для dict-wide events)
   * @param before previous record data (null для CREATE)
   * @param after proposed record data (null для CLOSE / WITHDRAW)
   */
  public void recordDraftEvent(
      String eventType, String action, DictionaryDefinition d,
      String businessKey, JsonNode before, JsonNode after) {
    try {
      AuditLog entry = new AuditLog();
      entry.setEventTime(OffsetDateTime.now());
      entry.setEventType(eventType);
      entry.setAction(action);
      entry.setDictionaryId(d.getId());
      entry.setDictionaryName(d.getName());
      if (businessKey != null) entry.setBusinessKey(businessKey);
      entry.setPayloadBefore(before);
      entry.setPayloadAfter(after);

      User user = currentUser();
      entry.setUserId(user.id);
      entry.setUserScope(user.scope);

      RequestContext rc = currentRequest();
      entry.setIpAddress(rc.ip);
      entry.setUserAgent(rc.userAgent);
      entry.setRequestId(rc.requestId);
      entry.setTraceId(MDC.get("traceId"));

      repository.save(entry);
    } catch (Exception ex) {
      // Mirror schema-draft path — audit failure must not break the user-facing
      // operation. Log + carry on; downstream observability picks up the gap.
      org.slf4j.LoggerFactory.getLogger(AuditLogger.class)
          .warn("recordDraftEvent audit write failed: type={} action={} dict={} bk={}",
              eventType, action, d.getName(), businessKey, ex);
    }
  }

  private void write(
      String eventType,
      String action,
      DictionaryDefinition d,
      DictionaryRecord r,
      JsonNode before,
      JsonNode after) {
    try {
      AuditLog entry = new AuditLog();
      entry.setEventTime(OffsetDateTime.now());
      entry.setEventType(eventType);
      entry.setAction(action);
      entry.setDictionaryId(d.getId());
      entry.setDictionaryName(d.getName());
      // Record-level events заполняют record_id / business_key; dict-level
      // events (DEFINITION_*) проходят с r=null — оставляем NULL'ы.
      if (r != null) {
        entry.setRecordId(r.getId());
        entry.setBusinessKey(r.getBusinessKey());
      }
      entry.setPayloadBefore(before);
      entry.setPayloadAfter(after);

      User user = currentUser();
      entry.setUserId(user.id);
      entry.setUserScope(user.scope);

      RequestContext rc = currentRequest();
      entry.setIpAddress(rc.ip);
      entry.setUserAgent(rc.userAgent);
      entry.setRequestId(rc.requestId);
      entry.setTraceId(MDC.get("traceId"));

      repository.save(entry);
    } catch (Exception ex) {
      // Аудит не должен ломать основной flow — но логируем громко чтобы
      // алертилось. Compliance-кейс ловит на этой ошибке.
      log.error(
          "Failed to write audit_log entry: dictionary={} record_id={} action={}: {}",
          d.getName(), r == null ? null : r.getId(), action, ex.getMessage(), ex);
    }
  }

  private User currentUser() {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    if (auth instanceof JwtAuthenticationToken jwt) {
      String sub = jwt.getToken().getSubject();
      if (sub == null || sub.isBlank()) {
        Object preferred = jwt.getToken().getClaim("preferred_username");
        sub = preferred == null ? jwt.getName() : String.valueOf(preferred);
      }
      Set<DataScope> scopes = scopeContext.currentScopes();
      DataScope effective = scopes.contains(DataScope.RESTRICTED) ? DataScope.RESTRICTED
          : scopes.contains(DataScope.INTERNAL) ? DataScope.INTERNAL
          : DataScope.PUBLIC;
      return new User(sub, effective);
    }
    return new User("anonymous", DataScope.PUBLIC);
  }

  private RequestContext currentRequest() {
    RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
    if (attrs instanceof ServletRequestAttributes sra) {
      HttpServletRequest req = sra.getRequest();
      String ip = headerOrRemote(req, "X-Forwarded-For");
      if (ip != null && ip.contains(",")) ip = ip.split(",")[0].trim();
      String requestId = req.getHeader("X-Request-Id");
      return new RequestContext(ip, req.getHeader("User-Agent"), requestId);
    }
    return new RequestContext(null, null, null);
  }

  private static String headerOrRemote(HttpServletRequest req, String header) {
    String v = req.getHeader(header);
    return (v == null || v.isBlank()) ? req.getRemoteAddr() : v;
  }

  private record User(String id, DataScope scope) {}
  private record RequestContext(String ip, String userAgent, String requestId) {}
}
