'use strict';

const crypto = require('crypto');
const { sortTimelineMessages, timelineTimestampMs } = require('./timeline-order');

const MESSAGE_ROLES = new Set(['user', 'assistant']);

// Below this length, identical text is too likely to be a legitimately-repeated short
// turn (e.g. a user typing "yes" twice) to safely use as a cross-timestamp dedup key.
const CONTENT_IDENTITY_MIN_LEN = 200;

function textHash(text) {
  return crypto.createHash('sha1').update(String(text || '')).digest('hex').slice(0, 16);
}

function messageText(message) {
  return String(message?.text ?? message?.content ?? message?.message ?? '').trim();
}

function timestampValue(timestamp) {
  if (timestamp == null || timestamp === '') return '';
  const raw = String(timestamp || '');
  const parsed = timelineTimestampMs(timestamp);
  if (parsed != null) {
    try { return new Date(parsed).toISOString(); } catch {}
  }
  return raw;
}

function messageKey(message, textHashValue) {
  const role = String(message?.role || '').trim();
  const text = messageText(message);
  if (!role || !text) return '';
  return [
    role,
    timestampValue(message?.timestamp),
    textHashValue || textHash(text),
  ].join(':');
}

function messageIdentityKeys(message, textHashValue) {
  const role = String(message?.role || '').trim();
  if (!MESSAGE_ROLES.has(role)) return [];
  const keys = [];
  const parentUuid = String(
    message?.parentUuid
    || message?._parent
    || message?.metadata?.parentUuid
    || message?.metadata?.uuid
    || ''
  ).trim();
  const ts = timestampValue(message?.timestamp);
  if (parentUuid) keys.push(['provider', role, parentUuid].join(':'));
  if (ts) keys.push(['timestamp', role, ts].join(':'));
  // Content identity: collapses an exact-duplicate long turn that shares neither a
  // parentUuid nor a timestamp with its twin — the provider-side hazards (stream-json
  // duplicate rows, a non-streaming fallback re-bundling blocks, or a session resume
  // rewriting history with fresh uuids/timestamps). Gated to long text so genuinely
  // repeated short turns are never collapsed.
  const text = messageText(message);
  if (text.length >= CONTENT_IDENTITY_MIN_LEN) {
    keys.push(['content', role, textHashValue || textHash(text)].join(':'));
  }
  return keys;
}

function messagesRepresentSameEvent(a, b) {
  const aText = messageText(a);
  const bText = messageText(b);
  if (!aText || !bText) return false;
  if (aText === bText) return true;
  const shorter = aText.length <= bText.length ? aText : bText;
  const longer = aText.length <= bText.length ? bText : aText;
  // The conversation cache intentionally stores long rows as bounded visible
  // text, while the live stream tail can still have the full JSONL row. Treat
  // a long cached prefix and the full stream event as the same turn. Keep this
  // bounded to long text so two short same-timestamp messages are not merged.
  return shorter.length >= 512 && longer.startsWith(shorter);
}

function eventRole(event) {
  const type = String(event?.type || '').trim();
  if (MESSAGE_ROLES.has(type)) return type;
  const role = String(event?.role || '').trim();
  return MESSAGE_ROLES.has(role) ? role : '';
}

function eventText(event) {
  return String(event?.text ?? event?.data?.text ?? '').trim();
}

function streamEventToMessage(event) {
  const role = eventRole(event);
  const text = eventText(event);
  if (!role || !text) return null;

  const message = {
    role,
    text,
    timestamp: timestampValue(event?.timestamp ?? event?.data?.timestamp ?? ''),
    source: event?.source || 'session-stream',
  };
  if (event?.eventId) message.streamEventId = event.eventId;
  if (Array.isArray(event?.data?.contentBlocks)) message.contentBlocks = event.data.contentBlocks;
  if (event?.data?.parentUuid) message.parentUuid = event.data.parentUuid;
  return message;
}

function sortMessagesChronologically(messages) {
  return sortTimelineMessages(messages)
    .map(({ _mergeSourceOrder, _mergeIndex, ...message }) => message);
}

function mergeMessagesWithStreamEvents(messages, events) {
  const merged = [];
  const seen = new Set();
  const byIdentity = new Map();
  let added = 0;
  let replaced = 0;

  const addMessage = (message, sourceOrder, index, fromStream = false) => {
    const text = messageText(message);
    const role = String(message?.role || '').trim();
    if (!role || !text) return;
    // Only user/assistant turns ever arrive via the live stream ring; durable
    // base messages with other roles (tool-result rows, structured-capture
    // system rows) must pass through, or a hot stream tail hides them. They
    // get exact-key dedup below but no identity-key merging (identity keys
    // are user/assistant-only).
    if (fromStream && !MESSAGE_ROLES.has(role)) return;
    const normalized = {
      ...message,
      role,
      text,
      timestamp: timestampValue(message?.timestamp),
      _mergeSourceOrder: sourceOrder,
      _mergeIndex: index,
    };
    const th = textHash(text);
    const key = messageKey(normalized, th);
    if (!key || seen.has(key)) return;
    const identityKeys = messageIdentityKeys(normalized, th);
    for (const identityKey of identityKeys) {
      if (!byIdentity.has(identityKey)) continue;
      const existing = byIdentity.get(identityKey);
      const existingMessage = merged[existing.index];
      // A shared provider key (parentUuid) means the SAME provider row: a still-streaming
      // partial in the live ring and its finalized copy in the cache. Collapse it
      // unconditionally (keep the longer text) — the partial is not always a clean >=512
      // prefix of the final, so messagesRepresentSameEvent's prefix gate can miss it.
      // Weaker keys (timestamp / content-hash) still require a text match so two genuinely
      // different turns that merely collide on one weak key are never merged.
      const sameEvent = identityKey.startsWith('provider:')
        || messagesRepresentSameEvent(existingMessage, normalized);
      if (sameEvent) {
        seen.add(key);
        if (text.length > messageText(existingMessage).length) {
          merged[existing.index] = normalized;
          replaced++;
        }
        identityKeys.forEach(k => byIdentity.set(k, existing));
        return;
      }
    }
    seen.add(key);
    identityKeys.forEach(identityKey => byIdentity.set(identityKey, { index: merged.length, fromStream }));
    merged.push(normalized);
    if (fromStream) added++;
  };

  (Array.isArray(messages) ? messages : []).forEach((message, index) => {
    addMessage(message, 0, index, false);
  });
  (Array.isArray(events) ? events : []).forEach((event, index) => {
    const message = streamEventToMessage(event);
    if (message) addMessage(message, 1, index, true);
  });

  return {
    messages: sortMessagesChronologically(merged),
    added,
    replaced,
  };
}

module.exports = {
  mergeMessagesWithStreamEvents,
  streamEventToMessage,
  timestampValue,
};
