import { getDb } from '@/shared/db';
import {
  enqueueOutbox,
  listDue,
  markSending,
  markResult,
  getOutbox,
  countByStatus,
  type ChannelPref,
  type OutboxResultInput,
} from '@/entities/outbox';
import { markRead } from '@/entities/conversation-read';

import { schedule, type Priority } from './scheduler';
import { timezoneForHandle } from './timezone';

/** Numeric weight per priority label — indexes `outbox.priority` for drain order. */
const PRIORITY_WEIGHT: Readonly<Record<Priority, number>> = {
  HOT: 100,
  HIGH: 75,
  NORMAL: 50,
  LOW: 25,
  STALE: 10,
};

export interface EnqueueReplyInput {
  recipient: string;
  body: string;
  channelPref?: ChannelPref;
  /** Epoch ms of the lead's last inbound — drives scheduling when set. */
  recvAt?: number | null;
  /** Explicit ISO send-eligible time; overrides the scheduler when set. */
  notBefore?: string | null;
  /** Dedupe key; defaults to recipient+body-hash+local-day. */
  dedupeKey?: string | null;
  maxAttempts?: number;
  /** When true, a confirmed send also marks the recipient's thread read. */
  markReadOnSend?: boolean;
}

export interface EnqueueReplyResult {
  id: string;
  deduped: boolean;
  priority: number;
  notBefore: string;
  reason: string;
}

// Small, stable, non-cryptographic hash for the dedupe key (FNV-1a).
function hashBody(body: string): string {
  let h = 0x811c9dc5;
  for (let i = 0; i < body.length; i++) {
    h ^= body.charCodeAt(i);
    h = Math.imul(h, 0x01000193);
  }
  return (h >>> 0).toString(36);
}

function defaultDedupeKey(recipient: string, body: string): string {
  const day = new Date().toISOString().slice(0, 10); // UTC day is fine for idempotency
  return `${recipient}:${hashBody(body)}:${day}`;
}

/**
 * Enqueue a triage reply. Picks a humane `not_before` + priority via the
 * scheduler (unless the caller pins `notBefore`), derives the recipient's
 * timezone from the area code, and dedupes so re-runs don't double-send.
 */
export async function enqueueReply(
  deviceId: string,
  input: EnqueueReplyInput,
): Promise<EnqueueReplyResult> {
  try {
    const now = Date.now();
    const tz = timezoneForHandle(input.recipient);

    // When the caller pins not_before we still derive a priority for queue
    // order; recvAt is used if present, else treat as low-urgency "send when due".
    const recvAt = input.recvAt ?? now - 24 * 60 * 60 * 1000;
    const decision = schedule({ recvAt, now, tz });

    const notBefore = input.notBefore ?? new Date(decision.notBefore).toISOString();
    const priority = PRIORITY_WEIGHT[decision.priority];
    const dedupeKey = input.dedupeKey ?? defaultDedupeKey(input.recipient, input.body);

    const { id, deduped } = await enqueueOutbox(getDb(), {
      deviceId,
      recipient: input.recipient,
      body: input.body,
      priority,
      channelPref: input.channelPref ?? 'auto',
      notBefore,
      maxAttempts: input.maxAttempts,
      dedupeKey,
      scheduleReason: decision.reason,
      markReadOnSend: input.markReadOnSend ?? false,
    });

    return { id, deduped, priority, notBefore, reason: decision.reason };
  } catch (err) {
    throw new Error(
      `enqueueReply failed for ${input.recipient}: ${err instanceof Error ? err.message : String(err)}`,
      { cause: err },
    );
  }
}

// Thin delegators — return the repo promise directly (errors already wrapped in
// the repo layer with operation context); no extra async frame to swallow them.
export function listDueReplies(deviceId: string, limit?: number) {
  return listDue(getDb(), deviceId, new Date().toISOString(), limit);
}

/** Returns false when the row isn't owned by `deviceId` (surface maps to 404). */
export function claimForSend(id: string, deviceId: string): Promise<boolean> {
  return markSending(getDb(), id, deviceId);
}

/**
 * Record a send result. Returns false when the row isn't owned by
 * `input.deviceId` (surface maps to 404). On a confirmed send of a row flagged
 * `mark_read_on_send`, also marks the recipient's thread read (logical-read
 * model). The read-mark is best-effort: it must never fail the result write
 * (Handoff 04 — "never block sends on it").
 */
export async function recordResult(input: OutboxResultInput): Promise<boolean> {
  try {
    const db = getDb();
    const ok = await markResult(db, input);
    if (ok && (input.status === 'sent' || input.status === 'delivered')) {
      const row = await getOutbox(db, input.id);
      if (row?.markReadOnSend) {
        await markRead(db, { deviceId: input.deviceId, handle: row.recipient, source: 'outbox_send' }).catch(
          (err) => {
            console.warn(`outbox: mark-read-on-send failed for ${input.id}: ${String(err)}`);
          },
        );
      }
    }
    return ok;
  } catch (err) {
    throw new Error(`recordResult failed for ${input.id}: ${err instanceof Error ? err.message : String(err)}`, {
      cause: err,
    });
  }
}

export function getReply(id: string) {
  return getOutbox(getDb(), id);
}

export function outboxStats(deviceId: string) {
  const db = getDb();
  return Promise.all([
    countByStatus(db, deviceId, 'queued'),
    countByStatus(db, deviceId, 'sending'),
    countByStatus(db, deviceId, 'sent'),
    countByStatus(db, deviceId, 'failed'),
  ]).then(([queued, sending, sent, failed]) => ({ queued, sending, sent, failed }));
}
