import { XMLParser } from 'fast-xml-parser';
import type { ParsedFeed, RawFeedItem } from '../types/feed.ts';

const xmlParser = new XMLParser({
  ignoreAttributes: false,
  attributeNamePrefix: '@_',
  cdataPropName: '__cdata',
  parseTagValue: false,
  trimValues: true,
});

export class ParseError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'ParseError';
  }
}

function textOf(v: unknown): string {
  if (v === null || v === undefined) return '';
  if (typeof v === 'string') return v;
  if (typeof v === 'object') {
    const obj = v as Record<string, unknown>;
    if (obj.__cdata) return String(obj.__cdata);
    if (obj['#text']) return String(obj['#text']);
    return '';
  }
  return String(v);
}

function asArray<T>(v: T | T[] | undefined | null): T[] {
  if (!v) return [];
  return Array.isArray(v) ? v : [v];
}

function parseRss(xml: any): ParsedFeed {
  const channel = xml?.rss?.channel || xml?.channel;
  if (!channel) throw new ParseError('not an RSS feed');
  const items: RawFeedItem[] = asArray<any>(channel.item).map((it) => ({
    title: textOf(it.title),
    link: textOf(it.link),
    guid: textOf(it.guid) || null,
    published: textOf(it.pubDate || it['dc:date'] || it.date),
    contentHtml: textOf(it['content:encoded'] || it.description),
    contentText: null,
    author: textOf(it['dc:creator'] || it.author) || null,
  }));
  return {
    type: 'rss',
    title: textOf(channel.title),
    description: textOf(channel.description),
    siteUrl: textOf(channel.link),
    items,
  };
}

function linkHref(l: any): string {
  if (!l) return '';
  if (typeof l === 'string') return l;
  if (Array.isArray(l)) {
    const alt = l.find((x: any) => x?.['@_rel'] === 'alternate') || l[0];
    return alt?.['@_href'] || alt?.['#text'] || '';
  }
  return l['@_href'] || l['#text'] || '';
}

function parseAtom(xml: any): ParsedFeed {
  const feed = xml?.feed;
  if (!feed) throw new ParseError('not an Atom feed');
  const items: RawFeedItem[] = asArray<any>(feed.entry).map((e) => ({
    title: textOf(e.title),
    link: linkHref(e.link),
    guid: textOf(e.id) || null,
    published: textOf(e.published || e.updated),
    contentHtml: textOf(e.content || e.summary),
    contentText: null,
    author: textOf(e.author?.name || e.author) || null,
  }));
  return {
    type: 'atom',
    title: textOf(feed.title),
    description: textOf(feed.subtitle),
    siteUrl: linkHref(feed.link),
    items,
  };
}

function parseJsonFeed(json: any): ParsedFeed {
  if (!json || !Array.isArray(json.items)) throw new ParseError('not a JSON Feed');
  const items: RawFeedItem[] = json.items.map((it: any) => ({
    title: it.title || '',
    link: it.url || it.external_url || '',
    guid: it.id || null,
    published: it.date_published || it.date_modified || '',
    contentHtml: it.content_html || '',
    contentText: it.content_text || null,
    author: (it.author && (it.author.name || it.author)) || null,
  }));
  return {
    type: 'json',
    title: json.title || '',
    description: json.description || '',
    siteUrl: json.home_page_url || '',
    items,
  };
}

export function parseFeed(text: string, contentType = ''): ParsedFeed {
  const trimmed = text.trim();
  const isJson = contentType.includes('json') || trimmed.startsWith('{');
  if (isJson) {
    let json: any;
    try {
      json = JSON.parse(trimmed);
    } catch {
      throw new ParseError('invalid JSON');
    }
    return parseJsonFeed(json);
  }
  let xml: any;
  try {
    xml = xmlParser.parse(trimmed);
  } catch (e) {
    throw new ParseError(`invalid XML: ${(e as Error).message}`);
  }
  if (xml?.rss) return parseRss(xml);
  if (xml?.feed) return parseAtom(xml);
  throw new ParseError('unknown feed format');
}
