import type { Querier } from '@platform-modules/db'
import type { ContentEntry } from './model.js'
import type { ContentSchema } from './schema.js'
import { list, type ListQuery, type StoreOpts } from './store.js'

export type FeedFormat = 'rss' | 'json'

export type FeedMeta = {
  title: string
  siteUrl: string
  feedUrl: string
  description?: string
}

function escapeXml(s: string): string {
  return s
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;')
}

function entryUrl(siteUrl: string, e: ContentEntry): string {
  return `${siteUrl.replace(/\/+$/, '')}/${e.type}/${e.slug}`
}

function publishedOnly(entries: ContentEntry[]): (ContentEntry & { publishedAt: Date })[] {
  return entries
    .filter((e): e is ContentEntry & { publishedAt: Date } => e.status === 'published' && e.publishedAt !== null)
    .sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime())
}

/** Pure projection of PUBLISHED entries to RSS 2.0 or JSON Feed 1.1. No deps, no I/O. */
export function toFeed(entries: ContentEntry[], format: FeedFormat, meta: FeedMeta): string {
  const items = publishedOnly(entries)

  if (format === 'json') {
    return JSON.stringify({
      version: 'https://jsonfeed.org/version/1.1',
      title: meta.title,
      home_page_url: meta.siteUrl,
      feed_url: meta.feedUrl,
      ...(meta.description ? { description: meta.description } : {}),
      items: items.map((e) => ({
        id: entryUrl(meta.siteUrl, e),
        url: entryUrl(meta.siteUrl, e),
        title: e.title,
        content_html: e.body,
        date_published: e.publishedAt.toISOString(),
        ...(e.terms.filter((t) => t.taxonomy === 'tag').map((t) => t.slug).length
          ? { tags: e.terms.filter((t) => t.taxonomy === 'tag').map((t) => t.slug) }
          : {}),
      })),
    })
  }

  const itemsXml = items
    .map((e) => {
      const url = entryUrl(meta.siteUrl, e)
      return [
        '    <item>',
        `      <title>${escapeXml(e.title)}</title>`,
        `      <link>${escapeXml(url)}</link>`,
        `      <guid isPermaLink="true">${escapeXml(url)}</guid>`,
        `      <pubDate>${e.publishedAt.toUTCString()}</pubDate>`,
        `      <description>${escapeXml(e.body)}</description>`,
        '    </item>',
      ].join('\n')
    })
    .join('\n')

  return [
    '<?xml version="1.0" encoding="UTF-8"?>',
    '<rss version="2.0">',
    '  <channel>',
    `    <title>${escapeXml(meta.title)}</title>`,
    `    <link>${escapeXml(meta.siteUrl)}</link>`,
    `    <description>${escapeXml(meta.description ?? meta.title)}</description>`,
    itemsXml,
    '  </channel>',
    '</rss>',
  ]
    .filter((line) => line !== '')
    .join('\n')
}

/**
 * Safe-by-construction public feed (visibility spec §3.1). Lists with the ANONYMOUS
 * viewer (`null`) then formats — so the ONLY entries it can ever syndicate are the
 * published+public anonymous-floor set. Use this for any public syndication route:
 * it has no viewer arg to get wrong, making the read-floor STRUCTURAL not disciplinary.
 * (`toFeed` remains for adopters who already hold an anonymous-listed array.)
 */
export async function feedFrom(
  db: Querier<ContentSchema>,
  query: ListQuery,
  format: FeedFormat,
  meta: FeedMeta,
  opts?: StoreOpts,
): Promise<string> {
  const entries = await list(db, query, null, opts)
  return toFeed(entries, format, meta)
}
