import type { Pool } from 'pg';

import { pgAll, pgGet } from '@/shared/db';

import type { ConversationReadState, MarkReadInput, ReadSource } from './types';

function wrap(err: unknown, op: string): Error {
  return new Error(
    `conversation-read/repo.${op} failed: ${err instanceof Error ? err.message : String(err)}`,
    { cause: err },
  );
}

/**
 * Mark a handle read through an instant (defaults to now). Upsert on
 * `(device_id, handle)`; `read_through` only advances (GREATEST), so re-marking
 * an already-read thread is an idempotent no-op. Returns the effective
 * `read_through` after the upsert.
 */
export async function markRead(pool: Pool, input: MarkReadInput): Promise<{ readThrough: string }> {
  try {
    const row = await pgGet<{ read_through: string }>(
      pool,
      `INSERT INTO macsync.conversation_read (device_id, handle, read_through, source)
       VALUES ($1, $2, COALESCE($3::timestamptz, now()), $4)
       ON CONFLICT (device_id, handle) DO UPDATE
         SET read_through = GREATEST(macsync.conversation_read.read_through, EXCLUDED.read_through),
             source       = EXCLUDED.source,
             read_at      = now(),
             updated_at   = now()
       RETURNING read_through`,
      [input.deviceId, input.handle, input.readThrough ?? null, input.source ?? 'manual'],
    );
    if (!row) throw new Error('upsert returned no row');
    return { readThrough: row.read_through };
  } catch (err) {
    throw wrap(err, 'markRead');
  }
}

/**
 * Mark read every thread whose latest message is outbound (i.e. Quinn has
 * replied) — marking it read through that reply's time. Single statement:
 * per conversation, take the newest message; if it's outbound, mark the lead's
 * handle (newest inbound `from_handle`) read. Idempotent via the same GREATEST
 * upsert. Returns the handles affected.
 */
export async function markAllRepliedRead(pool: Pool, deviceId: string): Promise<readonly string[]> {
  try {
    const rows = await pgAll<{ handle: string }>(
      pool,
      `INSERT INTO macsync.conversation_read (device_id, handle, read_through, source)
       SELECT $1, h.handle, latest.last_at, 'all_replied'
       FROM (
         SELECT DISTINCT ON (m.conversation_id)
           m.conversation_id, m.is_from_me, m.sent_at AS last_at
         FROM macsync.messages m
         WHERE m.device_id = $1
         ORDER BY m.conversation_id, m.sent_at DESC
       ) latest
       JOIN LATERAL (
         SELECT m2.from_handle AS handle
         FROM macsync.messages m2
         WHERE m2.conversation_id = latest.conversation_id
           AND m2.is_from_me = false
           AND m2.from_handle <> ''
         ORDER BY m2.sent_at DESC
         LIMIT 1
       ) h ON true
       WHERE latest.is_from_me = true
       ON CONFLICT (device_id, handle) DO UPDATE
         SET read_through = GREATEST(macsync.conversation_read.read_through, EXCLUDED.read_through),
             source       = 'all_replied',
             read_at      = now(),
             updated_at   = now()
       RETURNING handle`,
      [deviceId],
    );
    return rows.map((r) => r.handle);
  } catch (err) {
    throw wrap(err, 'markAllRepliedRead');
  }
}

/**
 * Read-state of one handle: the stored `read_through` (if any) plus the handle's
 * latest inbound time, and whether it is currently unread (inbound newer than
 * `read_through`, or never marked read while inbound exists).
 */
export async function getReadState(
  pool: Pool,
  deviceId: string,
  handle: string,
): Promise<ConversationReadState> {
  try {
    const row = await pgGet<{
      read_through: string | null;
      read_at: string | null;
      source: string | null;
      last_inbound_at: string | null;
    }>(
      pool,
      `SELECT
         cr.read_through,
         cr.read_at,
         cr.source,
         (SELECT MAX(m.sent_at) FROM macsync.messages m
           WHERE m.device_id = $1 AND m.from_handle = $2 AND m.is_from_me = false) AS last_inbound_at
       FROM (SELECT 1) one
       LEFT JOIN macsync.conversation_read cr
         ON cr.device_id = $1 AND cr.handle = $2`,
      [deviceId, handle],
    );
    const readThrough = row?.read_through ?? null;
    const lastInboundAt = row?.last_inbound_at ?? null;
    const unread =
      lastInboundAt !== null &&
      (readThrough === null || new Date(lastInboundAt) > new Date(readThrough));
    return {
      handle,
      readThrough,
      readAt: row?.read_at ?? null,
      source: (row?.source as ReadSource | null) ?? null,
      lastInboundAt,
      unread,
    };
  } catch (err) {
    throw wrap(err, 'getReadState');
  }
}
