import { Logger } from '../logger';
import { Prisma } from '@prisma/client';
import {
  SUPER_SYNC_OP_TYPES,
  SUPER_SYNC_SNAPSHOT_OP_TYPES,
  type SuperSyncOpType,
} from '@sp/shared-schema';

import {
  VectorClock,
  VectorClockComparison,
  compareVectorClocks,
  limitVectorClockSize,
  MAX_VECTOR_CLOCK_SIZE,
} from '@sp/sync-core';

const FULL_STATE_OP_TYPES: ReadonlySet<string> = new Set(SUPER_SYNC_SNAPSHOT_OP_TYPES);

/**
 * True when `opType` carries the user's full state (SYNC_IMPORT, BACKUP_IMPORT,
 * REPAIR) and therefore supersedes prior ops up to its serverSeq.
 */
export const isFullStateOpType = (opType: string): boolean =>
  FULL_STATE_OP_TYPES.has(opType);

// Re-export for consumers of this module
export {
  VectorClock,
  VectorClockComparison,
  compareVectorClocks,
  limitVectorClockSize,
  MAX_VECTOR_CLOCK_SIZE,
};

// Structured error codes for client handling
export const SYNC_ERROR_CODES = {
  // Validation errors (400)
  VALIDATION_FAILED: 'VALIDATION_FAILED',
  INVALID_OP_ID: 'INVALID_OP_ID',
  INVALID_OP_TYPE: 'INVALID_OP_TYPE',
  INVALID_ENTITY_TYPE: 'INVALID_ENTITY_TYPE',
  INVALID_ENTITY_ID: 'INVALID_ENTITY_ID',
  INVALID_PAYLOAD: 'INVALID_PAYLOAD',
  PAYLOAD_TOO_LARGE: 'PAYLOAD_TOO_LARGE',
  INVALID_VECTOR_CLOCK: 'INVALID_VECTOR_CLOCK',
  INVALID_TIMESTAMP: 'INVALID_TIMESTAMP',
  MISSING_ENTITY_ID: 'MISSING_ENTITY_ID',
  INVALID_SCHEMA_VERSION: 'INVALID_SCHEMA_VERSION',
  INVALID_CLIENT_ID: 'INVALID_CLIENT_ID',

  // Conflict errors (409)
  CONFLICT_CONCURRENT: 'CONFLICT_CONCURRENT',
  CONFLICT_SUPERSEDED: 'CONFLICT_SUPERSEDED',
  DUPLICATE_OPERATION: 'DUPLICATE_OPERATION',

  // Rate limiting (429)
  RATE_LIMITED: 'RATE_LIMITED',

  // Storage quota (413)
  STORAGE_QUOTA_EXCEEDED: 'STORAGE_QUOTA_EXCEEDED',

  // Encryption-related errors (400)
  ENCRYPTED_OPS_NOT_SUPPORTED: 'ENCRYPTED_OPS_NOT_SUPPORTED' as const,

  // Server errors (500)
  INTERNAL_ERROR: 'INTERNAL_ERROR',
} as const;

export type SyncErrorCode = (typeof SYNC_ERROR_CODES)[keyof typeof SYNC_ERROR_CODES];

export type ConflictType =
  | 'concurrent'
  | 'superseded'
  | 'equal_different_client'
  | 'unknown';

export interface ConflictResult {
  hasConflict: boolean;
  reason?: string;
  conflictType?: ConflictType;
  existingClock?: VectorClock;
}

// Operation types - single source of truth
export const OP_TYPES = SUPER_SYNC_OP_TYPES;

export type OpType = SuperSyncOpType;

// VectorClock, VectorClockComparison, and compareVectorClocks are imported from @sp/sync-core
// and re-exported above. This ensures client and server use identical implementations.

/**
 * Validates and sanitizes a vector clock.
 * Returns a sanitized clock with validated entries, or an error.
 *
 * Validation rules:
 * - Maximum 50 entries (prevents DoS via huge clocks)
 * - Keys must be non-empty strings, max 255 characters
 * - Values must be non-negative integers, capped at 100,000,000
 * - Invalid entries are removed (not rejected)
 */
export const sanitizeVectorClock = (
  clock: unknown,
): { valid: true; clock: VectorClock } | { valid: false; error: string } => {
  if (typeof clock !== 'object' || clock === null || Array.isArray(clock)) {
    return { valid: false, error: 'Vector clock must be an object' };
  }

  const entries = Object.entries(clock as Record<string, unknown>);

  // Reject absurdly large clocks (DoS protection).
  // Legitimate clocks can temporarily exceed MAX_VECTOR_CLOCK_SIZE during conflict
  // resolution: entity clock IDs + client ID + merged clocks from multiple concurrent
  // clients. 2.5x MAX gives room for multi-client merge scenarios while catching
  // adversarial inputs. Server-side pruning (limitVectorClockSize) will trim to MAX
  // before storage.
  const MAX_SANITIZE_VECTOR_CLOCK_SIZE = Math.ceil(MAX_VECTOR_CLOCK_SIZE * 2.5);
  if (entries.length > MAX_SANITIZE_VECTOR_CLOCK_SIZE) {
    return {
      valid: false,
      error: `Vector clock has too many entries (${entries.length}, max ${MAX_SANITIZE_VECTOR_CLOCK_SIZE})`,
    };
  }

  const sanitized: VectorClock = {};
  let strippedCount = 0;

  for (const [key, value] of entries) {
    // Validate key
    if (typeof key !== 'string' || key.length === 0 || key.length > 255) {
      strippedCount++;
      continue; // Skip invalid keys
    }

    // Validate value
    if (
      typeof value !== 'number' ||
      !Number.isInteger(value) ||
      value < 0 ||
      // Cap at 100M — impossibly large for normal use (would need ~1 op/second
      // for 3+ years) but prevents an adversarial client from sending a huge
      // counter that makes all other clocks LESS_THAN it.
      value > 100_000_000
    ) {
      strippedCount++;
      continue; // Skip invalid values
    }

    sanitized[key] = value;
  }

  if (strippedCount > 0) {
    Logger.warn(
      `sanitizeVectorClock: Stripped ${strippedCount} invalid entries from vector clock`,
    );
  }

  return { valid: true, clock: sanitized };
};

// compareVectorClocks is imported from @sp/sync-core (see imports at top of file)

export interface Operation {
  id: string;
  clientId: string;
  actionType: string;
  opType: OpType;
  entityType: string;
  entityId?: string;
  entityIds?: string[]; // For batch operations
  payload: unknown;
  vectorClock: VectorClock;
  timestamp: number;
  schemaVersion: number;
  isPayloadEncrypted?: boolean; // True if payload is E2E encrypted
  syncImportReason?: string;
}

export interface DuplicateOperationCandidate {
  id: string;
  userId: number;
  clientId: string;
  actionType: string;
  opType: string;
  entityType: string;
  entityId: string | null;
  payload: unknown;
  vectorClock: unknown;
  schemaVersion: number;
  clientTimestamp: bigint | number | string;
  receivedAt: bigint | number | string;
  isPayloadEncrypted: boolean;
  syncImportReason: string | null;
}

/**
 * The exact column set `isSameDuplicateOperation` needs to compare an incoming
 * op against a stored one. Shared by every duplicate-detection query (batch
 * prefetch + both legacy per-op checks) so a field added here can never be
 * silently missed at one of the call sites.
 */
export const DUPLICATE_OP_SELECT = {
  id: true,
  userId: true,
  clientId: true,
  actionType: true,
  opType: true,
  entityType: true,
  entityId: true,
  payload: true,
  vectorClock: true,
  schemaVersion: true,
  clientTimestamp: true,
  receivedAt: true,
  isPayloadEncrypted: true,
  syncImportReason: true,
} satisfies Prisma.OperationSelect;

export interface LatestEntityOperationRow {
  entityId: string;
  clientId: string;
  vectorClock: unknown;
}

export interface LatestBatchEntityOperationRow extends LatestEntityOperationRow {
  entityType: string;
}

export interface BatchUploadCandidate {
  op: Operation;
  resultIndex: number;
  originalTimestamp: number;
  fullStateVectorClock?: VectorClock;
  /**
   * UTF-8 byte size of `op.payload` captured during validation, reused when
   * sizing the stored op so a large payload isn't re-stringified. See
   * `computeOpStorageBytes`'s `cachedPayloadBytes` parameter.
   */
  payloadBytes?: number;
}

export interface AcceptedBatchOperation extends BatchUploadCandidate {
  serverSeq: number;
  storageBytes: number;
}

// Conservative enough to avoid planner-heavy BitmapOr + Sort plans on large
// histories while still replacing up to 100 per-entity round trips with one query.
export const CONFLICT_DETECTION_ENTITY_BATCH_SIZE = 100;

export interface ServerOperation {
  serverSeq: number;
  op: Operation;
  receivedAt: number;
}

// Upload types
export interface UploadOpsRequest {
  ops: Operation[];
  clientId: string;
  lastKnownServerSeq?: number;
  requestId?: string; // For request deduplication on retries
}

export interface UploadResult {
  opId: string;
  accepted: boolean;
  serverSeq?: number;
  error?: string;
  errorCode?: SyncErrorCode;
  /**
   * The existing entity's vector clock when rejecting due to conflict.
   * Allows clients to create LWW updates that dominate the server's state.
   */
  existingClock?: VectorClock;
}

/**
 * Internal return of the serial-path `processOperation`: the client-facing
 * `UploadResult` plus the op's storage size, computed once at the persist site,
 * so the caller can accumulate `acceptedDeltaBytes` without re-measuring the
 * (potentially multi-MB) payload. `storageBytes` / `fallback` are only
 * meaningful when `result.accepted` is true. Mirrors the batch path, which
 * returns `acceptedDeltaBytes` from `processOperationBatch`.
 */
export interface ProcessOperationResult {
  result: UploadResult;
  storageBytes: number;
  fallback: boolean;
}

export interface UploadOpsResponse {
  results: UploadResult[];
  newOps?: ServerOperation[];
  latestSeq: number;
  /**
   * True when piggybacked ops were limited (more ops exist on server).
   * Client should trigger a download to get the remaining operations.
   */
  hasMorePiggyback?: boolean;
}

// Download types
export interface DownloadOpsQuery {
  sinceSeq: number;
  limit?: number;
  excludeClient?: string;
}

export interface DownloadOpsResponse {
  ops: ServerOperation[];
  hasMore: boolean;
  latestSeq: number;
  /**
   * Set to true if operations were deleted and the client should re-sync
   * from a snapshot. This happens when:
   * - The requested sinceSeq is older than retained operations
   * - There's a gap in sequence numbers (operations were purged)
   */
  gapDetected?: boolean;
  /**
   * Aggregated vector clock from all ops before and including the snapshot.
   * Only set when snapshot optimization is used.
   * Clients need this to create merged updates that dominate all known clocks.
   */
  snapshotVectorClock?: VectorClock;
  /**
   * Server timestamp for client clock drift detection.
   */
  serverTime?: number;
}

// Status types
export interface SyncStatusResponse {
  latestSeq: number;
  devicesOnline: number;
  snapshotAge?: number;
  storageUsedBytes: number;
  storageQuotaBytes: number;
}

// Snapshot generation result (shared by SnapshotService + SnapshotGenerationService)
export interface SnapshotResult {
  state: unknown;
  serverSeq: number;
  generatedAt: number;
  schemaVersion: number;
}

// Payload validation result
export interface PayloadValidationResult {
  valid: boolean;
  error?: string;
}

/**
 * Validates operation payload structure based on operation type.
 * This is a server-side security check to ensure payloads have the expected structure.
 *
 * Note: The entity ID is stored in operation.entityId, NOT in the payload.
 * Payloads contain the entity data, e.g., { task: { id: '...', title: '...' } }
 *
 * Rules:
 * - CRT: Must be a non-null object (contains the entity being created)
 * - UPD: Must be an object (partial update)
 * - DEL: Can be empty object, null, or object with deletion metadata
 * - MOV: Must be an object (contains move/reorder data)
 * - BATCH: Must be an object, optionally with 'entities' object
 * - SYNC_IMPORT/BACKUP_IMPORT/REPAIR: Accept any (too complex to validate)
 */
export const validatePayload = (
  opType: OpType,
  payload: unknown,
): PayloadValidationResult => {
  // Skip validation for full-state operations (too complex to validate server-side)
  if (isFullStateOpType(opType)) {
    return { valid: true };
  }

  // DEL can have empty payload, null, or metadata object
  if (opType === 'DEL') {
    if (payload === null || payload === undefined) {
      return { valid: true };
    }
    if (typeof payload === 'object' && !Array.isArray(payload)) {
      return { valid: true };
    }
    // Encrypted DEL payload might be a string
    if (typeof payload === 'string') {
      return { valid: true };
    }
    return {
      valid: false,
      error: 'DEL payload must be null, an object, or an encrypted string',
    };
  }

  // Encrypted payloads are strings - allow them
  if (typeof payload === 'string') {
    return { valid: true };
  }

  // All other operations require an object payload
  if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
    return { valid: false, error: `${opType} payload must be a non-null object` };
  }

  const payloadObj = payload as Record<string, unknown>;

  // BATCH with 'entities' must have entities as an object
  if (opType === 'BATCH' && 'entities' in payloadObj) {
    if (
      typeof payloadObj.entities !== 'object' ||
      payloadObj.entities === null ||
      Array.isArray(payloadObj.entities)
    ) {
      return { valid: false, error: 'BATCH entities must be an object keyed by ID' };
    }
  }

  return { valid: true };
};

// Configuration
export interface SyncConfig {
  maxPayloadSizeBytes: number;
  uploadRateLimit: { max: number; windowMs: number };
  retentionMs: number; // Unified retention period for ops, devices, and validation
  maxClockDriftMs: number;
  batchUpload: boolean;
}

// Time constants (in milliseconds)
export const MS_PER_MINUTE = 60 * 1000;
export const MS_PER_HOUR = 60 * MS_PER_MINUTE;
export const MS_PER_DAY = 24 * MS_PER_HOUR;

// Retention period
export const RETENTION_DAYS = 45;
export const RETENTION_MS = RETENTION_DAYS * MS_PER_DAY;

// Device thresholds
export const ONLINE_DEVICE_THRESHOLD_MS = 5 * MS_PER_MINUTE; // 5 minutes

export const DEFAULT_SYNC_CONFIG: SyncConfig = {
  maxPayloadSizeBytes: 20 * 1024 * 1024, // 20MB - needed for large imports
  uploadRateLimit: { max: 100, windowMs: MS_PER_MINUTE },
  retentionMs: RETENTION_MS, // 45 days - used for ops, devices, and validation
  maxClockDriftMs: MS_PER_MINUTE, // 60 seconds
  batchUpload: false,
};
