// Shape adapter for the DM/chat surface — Phase C §4.3 of the Mastodon
// follow-up plan.
//
// Pixelfed and Mastodon both ship a "conversations" inbox plus a "thread"
// view, but the wire shapes differ enough that the screen would otherwise
// branch on `account.type` at every read site. This module normalizes both
// protocols into a single `ChatConversationRow` for the inbox and reuses the
// existing `RawMessage`/`ThreadResponse` shapes the conversation screen
// already consumes for the thread.
//
// The Pixelfed branch keeps its current behaviour (its inline shape is just
// renamed). The Mastodon branch:
//   - reads `Conversation.accounts[]` (already excludes self per Mastodon's
//     serializer) for the participant list
//   - reads `last_status.media_attachments[0]` for the photo-preview kind
//     and HTML-stripped `last_status.content` for the text preview
//   - reconstructs the thread from `GET /api/v1/statuses/:id/context` anchored
//     on `last_status.id`, mapping each Status to a `RawMessage`

import type { AccountType } from 'src/state/accountStore'
import { htmlToTextWithLineBreaks } from 'src/utils'
import type { MastodonContext, MastodonConversation } from './api/mastodon'
import type { Account, Status } from './api-types'

// --- Inbox row -------------------------------------------------------------

export type ChatPreviewKind =
  | 'text'
  | 'photo'
  | 'photo:album'
  | 'video'
  | 'audio'
  | 'gifv'
  | 'story'
  | 'empty'

export type ChatPreview =
  | { kind: 'text'; text: string; isSelf: boolean }
  | {
      kind: 'photo' | 'photo:album' | 'video' | 'audio' | 'gifv' | 'story'
      isSelf: boolean
      text?: string
    }
  | { kind: 'empty' }

export type ChatConversationRow = {
  /** Pixelfed: synthetic per-conversation id from the wire shape. Mastodon:
   * `Conversation.id`. Used for keyExtractor and any cache lookups. */
  id: string
  /** Value the screen passes as the route param to /chats/conversation/[id].
   * Pixelfed: the other participant's account id (current behaviour). Mastodon:
   * the Conversation.id (the thread query reads back from the conversations
   * cache to find the anchor `last_status.id`). */
  threadKey: string
  /** Every other party — for two-party threads `[0]` is the row label. */
  participants: Account[]
  /** Mastodon-only today; Pixelfed reports false because the wire shape does
   * not surface unread state. */
  unread: boolean
  preview: ChatPreview
  /** Surfaced for the timestamp; null when the thread has no messages yet. */
  lastStatus: { id: string; createdAt: string } | null
  origin: { type: AccountType; lastStatusId: string | null }
}

// --- Pixelfed inbox normalizer --------------------------------------------

/** Pixelfed's conversation row carries a `last_status` with `pf_type`,
 * `content_text`, and an `accounts[]` (other party first). The screen used
 * to read these inline; here we project to the shared shape. */
export function normalizePixelfedConversation(
  c: any,
  selfId: string
): ChatConversationRow {
  const status = c.last_status
  const isSelf = status?.account?.id != null && String(status.account.id) === selfId
  const cotype = status?.pf_type as string | undefined
  const cotext = status?.content_text as string | undefined

  let preview: ChatPreview
  if (!status) preview = { kind: 'empty' }
  else if (cotype === 'text') {
    const trimmed = (cotext ?? '').slice(0, 120)
    preview = { kind: 'text', text: trimmed, isSelf }
  } else if (cotype === 'story:reply' || cotype === 'story:reaction') {
    preview = { kind: 'story', isSelf }
  } else if (cotype === 'photo' || cotype === 'photo:album') {
    preview = { kind: cotype, isSelf }
  } else {
    // Unknown pf_type — fall through to whatever text we have.
    preview = { kind: 'text', text: (cotext ?? '').slice(0, 120), isSelf }
  }

  const other: Account | undefined = c.accounts?.[0]
  return {
    id: String(other?.id ?? c.id ?? Math.random()),
    threadKey: String(other?.id ?? ''),
    participants: c.accounts ?? [],
    unread: false,
    preview,
    lastStatus: status ? { id: String(status.id), createdAt: status.created_at } : null,
    origin: {
      type: 'pixelfed',
      lastStatusId: status ? String(status.id) : null,
    },
  }
}

// --- Mastodon inbox normalizer --------------------------------------------

const MASTODON_PREVIEW_TRUNCATE = 120

function stripHtmlToShortText(html: string): string {
  const text = htmlToTextWithLineBreaks(html).trim()
  if (text.length <= MASTODON_PREVIEW_TRUNCATE) return text
  return text.slice(0, MASTODON_PREVIEW_TRUNCATE) + '…'
}

/** Mastodon's `Conversation.accounts[]` already excludes self
 * (`account_conversation.rb#participant_account_ids`), so the row's "other
 * party" is `accounts[0]` for two-party threads — same convention as the
 * existing Pixelfed UI. Multi-party threads carry every other participant
 * here; the screen renders them with a `+N` suffix in the row label. */
export function normalizeMastodonConversation(
  c: MastodonConversation,
  selfId: string
): ChatConversationRow {
  const status = c.last_status
  const isSelf = !!status && String(status.account?.id ?? '') === selfId

  let preview: ChatPreview
  if (!status) {
    preview = { kind: 'empty' }
  } else if ((status.media_attachments?.length ?? 0) > 0) {
    const first = status.media_attachments[0]
    const t = first.type as string
    let kind: 'photo' | 'photo:album' | 'video' | 'audio' | 'gifv'
    if (t === 'video' || t === 'audio' || t === 'gifv') {
      kind = t
    } else if (t === 'image') {
      kind = status.media_attachments.length > 1 ? 'photo:album' : 'photo'
    } else {
      kind = 'photo'
    }
    preview = {
      kind,
      isSelf,
      text: status.content ? stripHtmlToShortText(status.content) : undefined,
    }
  } else {
    preview = {
      kind: 'text',
      text: stripHtmlToShortText(status.content ?? ''),
      isSelf,
    }
  }

  return {
    id: c.id,
    threadKey: c.id,
    participants: c.accounts,
    unread: c.unread,
    preview,
    lastStatus: status ? { id: status.id, createdAt: status.created_at } : null,
    origin: { type: 'mastodon', lastStatusId: status?.id ?? null },
  }
}

// --- Thread normalizer (Mastodon → existing ThreadResponse shape) ---------

/** The conversation screen consumes `RawMessage[]` and a small
 * `ThreadResponse` envelope (id/name/username/avatar/messages). Today the
 * Pixelfed `fetchChatThread` returns this shape directly. The Mastodon
 * adapter below packages the same shape from the participant Account + the
 * `/api/v1/statuses/:id/context` payload + the anchor status itself. */

export type ChatThreadEnvelope = {
  id: string
  name?: string | null
  username: string
  /** Webfinger handle (`alice@host` for remote actors, `alice` for local).
   * The Mastodon DM send path needs this to construct `@user@host` mentions
   * — `username` alone drops the host for federated accounts. Optional so
   * Pixelfed thread responses (which don't carry it) keep working. */
  acct?: string
  avatar: string
  messages: ChatThreadRawMessage[]
}

export type ChatThreadRawMessage = {
  id: string
  created_at: string
  type: string
  text?: string
  media?: string
  isAuthor?: boolean
  reportId?: string
}

function statusToRawMessage(s: Status, selfId: string): ChatThreadRawMessage {
  const isAuthor = String(s.account?.id ?? '') === selfId
  const firstMedia = s.media_attachments?.[0]
  if (firstMedia?.url) {
    return {
      id: s.id,
      created_at: s.created_at,
      type: 'photo',
      text: s.content ? htmlToTextWithLineBreaks(s.content).trim() : '',
      media: firstMedia.url,
      isAuthor,
    }
  }
  return {
    id: s.id,
    created_at: s.created_at,
    type: 'text',
    text: htmlToTextWithLineBreaks(s.content ?? '').trim(),
    isAuthor,
  }
}

export function normalizeMastodonThread(
  context: MastodonContext,
  anchor: Status,
  participant: Account,
  selfId: string
): ChatThreadEnvelope {
  // Order: ancestors (oldest first) + anchor + descendants. The screen sorts
  // ascending by created_at after building, but providing the right order
  // up front keeps the cache shape predictable.
  const all: Status[] = [
    ...(context.ancestors ?? []),
    anchor,
    ...(context.descendants ?? []),
  ]
  return {
    id: participant.id,
    name: participant.display_name ?? null,
    username: participant.username,
    acct: participant.acct,
    avatar: participant.avatar,
    messages: all.map((s) => statusToRawMessage(s, selfId)),
  }
}
