/* ============================= MastoAPI Types ============================= */

/* Date string like 2025-01-01T10:34:12.000000Z */
export type Timestamp = string

export type Visibility = 'public' | 'unlisted' | 'private' | 'direct' | 'draft'

/** Mastodon Pass §1.5 #25 / §3.4: forks may emit values outside this enum (e.g.
 * Glitch-soc's `local`). Renderers must treat unknown visibility values as
 * `public` rather than crashing. */
export type MediaType = 'unknown' | 'image' | 'gifv' | 'video' | 'audio'

export type PreviewCardType = 'link' | 'photo' | 'video' | 'rich'

export type Application = {
  name: string
  website: string | undefined
  scopes: Array<string>
  redirect_uris: Array<string>
  // Deprecated:
  redirect_uri: string | undefined
  vapid_key: string | undefined
}

/** Mastodon Pass §3.4: Pixelfed adds attachment-level extensions (notably
 * `pf_type` on the parent Status); Mastodon clients must fall back to this
 * `type` field. The carousel's video branch reads `post.pf_type === 'video'`
 * and falls back to `media.type === 'video'` for Mastodon contexts. */
export type MediaAttachment = {
  id: string
  type: MediaType
  url: string
  preview_url: string | null
  remote_url: string | null
  meta: MediaMetadata
  description: string | null
  blurhash: string | null
  // Deprecated:
  text_url: string | undefined
}

export type MediaMetadata = any // TODO

export type Mention = {
  id: string
  username: string
  url: string
  acct: string
}

/**
 * Mastodon-canonical `Tag` entity (`/api/v1/tags/:name`). Pixelfed returns
 * the same shape and adds an extended `count` field when `_pe=1` is set.
 * Optional fields are absent on the bare `Tag` you get embedded in a status
 * — the per-hashtag endpoint fills them in.
 */
export type Tag = {
  name: string
  url: string
  // Filled in by `/api/v1/tags/:name`; absent on inline-embedded tags.
  history?: Array<{ day: string; uses: string; accounts: string }>
  following?: boolean
  featured?: boolean
  // Pixelfed-extended (with `_pe=1`).
  count?: number
}

export type CustomEmoji = {
  shortcode: string
  url: string
  static_url: string
  visible_in_picker: boolean
  category: string | null
}

export type PreviewCard = {
  url: string
  title: string
  description: string
  language?: string | null
  type: PreviewCardType
  authors: Array<PreviewCardAuthor>
  author_name: string
  author_url: string
  provider_name: string
  provider_url: string
  html: string
  width: number
  height: number
  image: string | null
  image_description?: string
  embed_url: string
  blurhash: string | null
  published_at?: string | null
  // True iff the origin's fediverse:creator meta pointed at our instance
  // but the canonical URL lives on a different domain.
  missing_attribution?: boolean
}

export type PreviewCardAuthor = {
  name: string
  url: string
  account: Account | null
}

/** Mastodon Account `fields` entry — the top-level, public, rendered form (NOT
 * the editable `source.fields`). `value` is HTML: plain text (e.g. "they/them")
 * or an anchor (often with rel="me"). `verified_at` non-null means the linked
 * page carries a rel="me" backlink to this profile → render a verified marker on
 * the field. `name` and `value` may contain custom-emoji shortcodes resolved
 * against `Account.emojis`. Pixelfed always returns `fields: []`, so the UI
 * naturally renders nothing for Pixelfed accounts. */
export type AccountField = {
  name: string
  /** HTML — render via StatusContent (htmlToTextWithLineBreaks → AutolinkText). */
  value: string
  verified_at: string | null
}

export type Poll = {
  id: string
  expires_at: Timestamp | null
  expired: boolean
  multiple: boolean
  votes_count: number
  voters_count: number | null
  options: PollOptions
  emojis: Array<CustomEmoji>
  voted: boolean
  own_votes: Array<number>
}

export type PollOptions = {
  title: string
  votes_count: number | null
}

export type Relationship = {
  blocking: boolean
  domain_blocking
  endorsed: boolean
  followed_by?: boolean
  following: boolean
  following_since
  id
  muting: boolean
  muting_notifications
  requested: boolean
  showing_reblogs
}

export type RelationshipFromFollowAPIResponse = {
  blocking: boolean
  domain_blocking: any | null
  endorsed: boolean
  followed_by?: boolean
  following: boolean
  id: string
  muting: boolean
  muting_notifications: any | null
  requested: boolean
  showing_reblogs: any | null
}

export type Account = {
  /** value is username */
  acct: string
  avatar: string
  created_at: Timestamp
  discoverable: boolean
  display_name: string
  /** Mastodon-only (4.0+): user has chosen to hide their follows/followers
   * collections from the public profile. Absent on Pixelfed accounts. */
  hide_collections?: boolean
  /** Mastodon-only (4.2+): user has opted in to search-engine indexing.
   * Absent on Pixelfed accounts (Pixelfed exposes the inverse `crawlable`
   * field under `settings` instead). */
  indexable?: boolean
  /** Custom server emoji declared for the actor's name/bio. Absent when the
   * instance predates the Halflight account-emoji patch. */
  emojis?: Array<CustomEmoji>
  /** Mastodon public profile metadata fields (≤4 on stock Mastodon; remote
   * actors can exceed). Absent/empty on Pixelfed — the UI renders nothing.
   * Distinct from the editable `source.fields` form representation. */
  fields?: Array<AccountField>
  followers_count: number
  following_count: number
  header_bg: string | null
  id: string
  is_admin: boolean
  last_fetched_at: null
  local //: true,
  location // : null
  locked: boolean
  /** bio */
  note: string
  /** bio */
  note_text: string
  pronouns: string[]
  source: LoginUserSource
  /** url of profile, for pixelfed: https://<instance>/<username> */
  url: string
  username: string
  website: string
  /** Halflight extension: origin platform + AP actor type. Populated on the
   * backend only for non-Person remote actors; absent for stock-Pixelfed locals. */
  pf_source?: PfSource
  /** Halflight extension: number of upcoming events, populated for Group
   * actors so the app can render a community-style profile. */
  upcoming_events_count?: number
}

/** this is saved */
export interface LoginUserResponse extends Account {
  statuses_count: number
  settings: LoginUserSettings
}

export type LoginUserSettings = {
  crawlable: boolean
  disable_embeds: boolean
  high_contrast_mode: boolean
  indexable: boolean
  is_suggestable: boolean
  media_descriptions: boolean
  public_dm: boolean
  reduce_motion: boolean
  show_atom: boolean
  show_profile_follower_count: boolean
  show_profile_following_count: boolean
  video_autoplay: boolean
}

export type LoginUserSource = {
  privacy: Visibility
  sensitive: boolean
  /** language code like 'en' */
  language: string
  /** bio */
  note: string
  fields: any[]
}

export type Status = {
  id: string
  created_at: Timestamp
  in_reply_to_id: string | null
  in_reply_to_account_id: string | null
  sensitive: boolean
  spoiler_text: string
  visibility: Visibility
  language: string | null
  uri: string
  url: string | null
  reply_count: number
  /** Mastodon's plural form of `reply_count`. Pixelfed servers stamp the
   * singular field; Mastodon stamps the plural one. Use `getReplyCount(s)`
   * from `src/utils.ts` when reading — it handles the fallback. */
  replies_count?: number
  reblogs_count: number
  favourites_count: number
  edited_at: Timestamp | null
  favourited: boolean | undefined
  liked_at: Timestamp | null
  reblogged: boolean | undefined
  muted: boolean | undefined
  bookmarked: boolean | undefined
  pinned: boolean | undefined
  filtered: boolean | undefined
  content: string
  reblog: Status | null
  /** Mastodon Pass §3.4: Mastodon nullifies `application` for statuses where
   * the originating client is unknown; Pixelfed always populates it. Treat
   * `null`/`undefined` as "client unknown" — never assume a Status carries an
   * Application. */
  application: Application | null | undefined
  account: Account
  media_attachments: Array<MediaAttachment>
  mentions: Array<Mention>
  tags: Array<Tag>
  emojis: Array<CustomEmoji>
  card: Array<PreviewCard>
  poll: Poll | null
  local: boolean | undefined
  liked_by: StatusLikedBy | null
  /** Pixelfed-only (Mastodon Pass §3.2). Carries Pixelfed's structural
   * post-type discriminator. Absent on Mastodon responses — branches reading
   * this must fall back to `MediaAttachment.type` (e.g. video detection at
   * `FeedPost.tsx:128`, `PostGridCell.tsx:43`). */
  pf_type:
    | 'photo'
    | 'photo:album'
    | 'video'
    | 'video:album'
    | 'text'
    | 'reply'
    | 'event'
    | 'article'
    | 'quote'
    | undefined
  /** Pixelfed-only (Mastodon Pass §3.2). Federation-trifecta enrichment for
   * AS2 `Event` actors; never present on Mastodon responses. */
  pf_event?: PfEvent | null
  /** Pixelfed-only (Mastodon Pass §3.2). Federation-trifecta enrichment for
   * AS2 `Article` objects; never present on Mastodon responses. */
  pf_article?: PfArticle | null
  /** Pixelfed-only (Mastodon Pass §3.2). Carries the AP origin platform +
   * actor type for non-Person remote actors; never present on Mastodon. */
  pf_source?: PfSource
  /** Mastodon 4.4+ read-only (Mastodon Pass §3.2). Pixelfed-extended quotes
   * also flow through this shape per the trifecta work. Renderers must check
   * `Quote.state === 'accepted'` before rendering — every other state
   * collapses to "show nothing" so private data never leaks via a state
   * handling bug (plan §1.5 #33). */
  quote?: Quote | ShallowQuote | null
  quotes_count?: number
  quote_approval?: QuoteApproval
}

/* ======================= Halflight Status extensions ====================== */

export type PfSourcePlatform =
  | 'pixelfed'
  | 'mastodon'
  | 'mobilizon'
  | 'friendica'
  | 'pleroma'
  | 'akkoma'
  | 'gotosocial'
  | 'gancio'
  | 'wordpress'
  | 'other'

export type PfActorType = 'Person' | 'Group' | 'Organization' | 'Service' | 'Application'

export type PfSource = {
  platform: PfSourcePlatform
  remote_url: string | null
  software_version: string | null
  /** AP actor type, when the source is a remote actor — lets the client
   * branch on Group/Organization profiles without re-fetching the actor JSON. */
  actor_type?: PfActorType
}

export type PfEventTag = {
  slug: string
  title: string
}

export type PfEventRawOrganizer = {
  actor_uri: string
  name?: string | null
  type?: string | null
}

export type PfEventOrganizer = Account | PfEventRawOrganizer

export type PfEventLocation = {
  name: string | null
  address: string | null
  lat: number | null
  lon: number | null
  online_url: string | null
}

export type PfEventJoinMode = 'free' | 'restricted' | 'invite' | 'external'

export type PfEventStatus = 'confirmed' | 'tentative' | 'cancelled'

export type PfEventParticipationStatus =
  | 'going'
  | 'going_pending'
  | 'interested'
  | 'declined'

export type PfEventParticipation = {
  status: PfEventParticipationStatus | null
  updated_at: string | null
  cancellation_token: string | null
}

export type PfEvent = {
  name: string
  summary: string | null
  start_time: string
  end_time: string | null
  is_open_ended: boolean
  timezone: string | null
  location: PfEventLocation | null
  // Server returns hydrated Mastodon-shape Accounts when the organizer actor
  // has been federated locally, otherwise the raw AP shape from ingestion.
  // Clients must branch (see `isHydratedAccount` in `src/lib/events.ts`).
  organizers: Array<PfEventOrganizer>
  /** Co-hosts / contact actors (Mobilizon `Event.contacts`). Absent on stock
   * Mobilizon; populated by the backend once P2.4 ships. */
  contacts?: Array<Account>
  attendees_count: number | null
  interested_count: number | null
  maximum_attendee_capacity: number | null
  remaining_attendee_capacity: number | null
  join_mode: PfEventJoinMode | null
  external_url: string | null
  allow_comments: boolean
  allow_remote_interest: boolean
  /** From Mobilizon `EventOptions.anonymousParticipation`. When true, the
   * origin allows anonymous RSVP via its web UI — used to surface an
   * "Open on origin" CTA in the read-only URL-paste fallback. */
  allow_anonymous_participation?: boolean
  hide_attendee_count: boolean
  has_attendee_list: false
  status: PfEventStatus | null
  my_participation: PfEventParticipation | null
  /** BCP-47 language tag from AP `@language` / `inLanguage`. */
  language?: string | null
  /** True when the event spans multiple days. May be inferred from the
   * start/end delta on the client when the backend field is absent. */
  long_event?: boolean
  /** Mobilizon event category (single value). */
  category?: string | null
  /** Hashtag-style tags from AP `tag[]` filtered to `type: "Hashtag"`. */
  tags?: Array<PfEventTag>
}

export type PfArticle = {
  name: string
  canonical_url: string | null
  word_count: number | null
  reading_time_minutes: number | null
  authors: Array<Account>
}

export type QuoteState =
  | 'accepted'
  | 'pending'
  | 'rejected'
  | 'revoked'
  | 'deleted'
  | 'unauthorized'
  | 'blocked_account'
  | 'blocked_domain'
  | 'muted_account'

export type Quote = {
  state: QuoteState
  quoted_status?: Status
}

export type ShallowQuote = {
  state: QuoteState
  quoted_status_id?: string
}

export type QuoteApprovalPolicy = 'public' | 'followers' | 'nobody'

export type QuoteApproval = {
  automatic: string[]
  manual: string[]
  current_user: 'automatic' | 'manual' | 'denied' | 'unknown'
}

/* Mastodon-standard + Halflight notification kinds. Unknown types fall
 * through to the default renderer; keep this list authoritative. */
export type NotificationType =
  | 'mention'
  | 'reblog'
  | 'follow'
  | 'follow_request'
  | 'favourite'
  | 'poll'
  | 'status'
  | 'update'
  | 'admin.sign_up'
  | 'admin.report'
  | 'quote'
  | 'quoted_update'
  | 'severed_relationships'
  | 'moderation_warning'
  | 'event:rsvp'
  | 'event:update'
  | 'event:cancelled'
  | 'event:comment'

export type StatusLikedBy = {
  id: string
  others: boolean
  total_count: number
  total_count_pretty: number
  url: string
  username: string
}

/* ========================== Infinite query types ========================== */

export type PaginatedStatus = {
  data: Array<Status>
  nextPage: string | undefined
  prevPage: string | undefined
}

export type UploadV2Response = {
  blurhash: string
  description: string
  id: string
  preview_url: string
  remote_url: string
  text_url: string
  type: string
  url: string
}

export type UploadV2ErrorResponse = {
  errors: {
    file: string[]
  }
  message: string
}

export type UploadV2ResponseOrError = UploadV2Response | UploadV2ErrorResponse
export type UploadV2Params =
  | {
      file: unknown
    }
  | {
      file: unknown
      description: string
    }

interface OpenServer {
  domain: string
  header_thumbnail: string
  mobile_registration: boolean
  version: string
  short_description: string
  user_count: number
  last_seen_at: string
}

export type OpenServersResponse = Array<OpenServer>

/* ============================ Instance / Compose =========================== */

/** `GET /api/v1/instance` — subset we care about in the composer. */
export type InstanceConfiguration = {
  statuses: {
    max_characters: number
    max_media_attachments: number
    characters_reserved_per_url?: number
  }
  media_attachments: {
    supported_mime_types?: Array<string>
    image_size_limit?: number
    image_matrix_limit?: number
    description_limit?: number
    /** Server-enforced maximum bytes per uploaded video. */
    video_size_limit?: number
    video_frame_rate_limit?: number
    video_matrix_limit?: number
  }
  polls?: Record<string, unknown>
  /** Mastodon 4.4+ (Mastodon Pass §3.3): VAPID public key for RFC 8030 Web
   * Push subscriptions. Phase 4 reads this to register Mastodon push. */
  vapid?: {
    public_key?: string
  }
  /** Mastodon 4.5+ (plan §1.1): which timelines require auth. */
  timelines_access?: {
    public?: 'unauthenticated' | 'authenticated'
    local?: 'unauthenticated' | 'authenticated'
    hashtag?: 'unauthenticated' | 'authenticated'
    trending?: 'unauthenticated' | 'authenticated'
  }
  /** Mastodon (plan §1.5 #40): canonical streaming WebSocket URL. */
  urls?: {
    streaming?: string
  }
}

export type InstanceV1 = {
  uri?: string
  title?: string
  short_description?: string
  description?: string
  email?: string
  version?: string
  urls?: Record<string, string>
  stats: Record<string, number>
  thumbnail?: string
  languages?: Array<string>
  configuration: InstanceConfiguration
  approval_required?: boolean
  registrations?: boolean
  invites_enabled?: boolean
  contact_account: Account
  rules: Array<unknown>
}

/** Mastodon Pass §3.3: `GET /api/v2/instance`. Mastodon 4.4+ guarantees this
 * endpoint is reachable even on limited-federation servers (PR #34576).
 * Subset of fields the capability layer reads; the server payload is much
 * larger but the rest is not load-bearing for v1. */
export type InstanceV2 = {
  domain?: string
  title?: string
  version?: string
  source_url?: string
  description?: string
  thumbnail?: {
    url?: string
    blurhash?: string
  }
  languages?: Array<string>
  /** Mastodon 4.3+ (plan §1.1): opaque integer for capability gating. The
   * value-to-version mapping is deliberately unpublished — treat as a
   * monotonically-increasing comparator. */
  api_versions?: {
    mastodon?: number
  }
  configuration: InstanceConfiguration
  registrations?: {
    enabled?: boolean
    approval_required?: boolean
    message?: string | null
  }
  contact?: {
    email?: string
    account?: Account
  }
  rules?: Array<unknown>
}

/** `GET /api/pixelfed/v1/newsroom/timeline` — unread admin-authored posts.
 * `published_at` is pre-formatted server-side (e.g. "January 15, 2026");
 * `url` is null when the post is not externally linkable. */
export type NewsroomItem = {
  id: number
  title: string
  summary: string | null
  url: string | null
  published_at: string
}

/** `GET /api/v1.1/compose/settings`. Halflight-typed subset. */
export type ComposeSettings = {
  default_scope: Visibility
  max_media_attachments: number
  /** Server sends this as a string in some releases — we coerce to number on
   * read. Keep it numeric in types to remove `as any` at call sites. */
  max_altext_length: number
  /** Allowed quote approval policies. Absent on older Pixelfed. */
  quote_policy?: QuoteApprovalPolicy
  [key: string]: unknown
}
