import sanitizeHtml from 'sanitize-html';
import crypto from 'node:crypto';
import type { DedupKey, NormalizedFeed, NormalizedItem, ParsedFeed } from '../types/feed.ts';

const ALLOWED_TAGS = [
  'p',
  'br',
  'a',
  'ul',
  'ol',
  'li',
  'blockquote',
  'code',
  'pre',
  'strong',
  'em',
  'h1',
  'h2',
  'h3',
  'h4',
  'h5',
  'h6',
  'img',
  'figure',
  'figcaption',
];
const ALLOWED_ATTR = {
  a: ['href', 'title', 'rel', 'target'],
  img: ['src', 'alt', 'title'],
};

export function sanitizeContent(html: unknown): string {
  if (!html) return '';
  return sanitizeHtml(String(html), {
    allowedTags: ALLOWED_TAGS,
    allowedAttributes: ALLOWED_ATTR,
    transformTags: {
      a: (tagName, attribs) => ({
        tagName,
        attribs: {
          ...attribs,
          rel: 'noopener noreferrer nofollow',
          target: '_blank',
        },
      }),
    },
  });
}

function stripTags(html: unknown): string {
  return sanitizeHtml(String(html || ''), { allowedTags: [], allowedAttributes: {} }).trim();
}

function clampDate(raw: string | null | undefined, fetchTime?: string | Date): string {
  const now = fetchTime ? new Date(fetchTime) : new Date();
  if (!raw) return now.toISOString();
  const d = new Date(raw);
  if (Number.isNaN(d.getTime())) return now.toISOString();
  if (d.getTime() > now.getTime()) return now.toISOString();
  return d.toISOString();
}

function canonicalLink(link: string | null | undefined, baseUrl: string): string {
  if (!link) return '';
  try {
    return new URL(link, baseUrl).toString();
  } catch {
    return link;
  }
}

function hashKey(parts: string[]): string {
  return crypto.createHash('sha256').update(parts.join('\u0000')).digest('hex');
}

export function normalizeFeed(
  parsed: ParsedFeed,
  baseUrl: string,
  fetchTime?: string,
): NormalizedFeed {
  const ft = fetchTime ? new Date(fetchTime) : new Date();
  const items: NormalizedItem[] = (parsed.items || []).map((it) => {
    const link = canonicalLink(it.link, baseUrl);
    const title = (it.title || '').trim() || '(untitled)';
    const publishedAt = clampDate(it.published, ft);
    const contentHtml = sanitizeContent(it.contentHtml);
    const guid = it.guid ? String(it.guid).trim() : null;
    let dedupKey: DedupKey;
    if (guid) {
      dedupKey = `guid:${guid}`;
    } else {
      dedupKey = `hash:${hashKey([link, title, publishedAt])}`;
    }
    return {
      title,
      link,
      guid: guid || null,
      publishedAt,
      contentHtml,
      contentText: it.contentText ? String(it.contentText) : stripTags(contentHtml) || null,
      author: it.author ? String(it.author).trim() : null,
      dedupKey,
    };
  });
  return {
    title: parsed.title || '',
    description: parsed.description || '',
    siteUrl: parsed.siteUrl ? canonicalLink(parsed.siteUrl, baseUrl) : baseUrl,
    items,
  };
}
