import { readdirSync, readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import sitemap from '@astrojs/sitemap';
import { defineConfig } from 'astro/config';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeSlug from 'rehype-slug';
import { visit } from 'unist-util-visit';

// Build a lookup of article slugs to their last modification dates so the
// sitemap can advertise accurate <lastmod> values to crawlers. astro:content
// isn't available inside the config, so we read entry frontmatter directly.
// Each entry uses a single-line top-level scalar `date:` key, so a small regex
// extraction is sufficient and intentional. (`updated:` now lives nested under
// `article:`; no entry sets it, so falling back to `date` is correct.)
const postsDir = path.resolve(
  path.dirname(fileURLToPath(import.meta.url)),
  'src/content/work'
);

function extractScalar(frontmatter, key) {
  const match = frontmatter.match(new RegExp(`^${key}:\\s*(.+?)\\s*$`, 'm'));
  return match?.[1]?.replace(/^['"]|['"]$/g, '');
}

const postLastmodLookup = new Map(
  readdirSync(postsDir, { withFileTypes: true })
    .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
    .map((entry) => {
      const raw = readFileSync(path.join(postsDir, entry.name), 'utf8');
      const frontmatter = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? '';
      const rawDate =
        extractScalar(frontmatter, 'updated') ?? extractScalar(frontmatter, 'date');
      const parsed = rawDate ? new Date(rawDate) : null;
      const valid = parsed && !Number.isNaN(parsed.valueOf()) ? parsed : null;
      return [entry.name.replace(/\.md$/, ''), valid];
    })
    .filter(([, date]) => date !== null)
);

export default defineConfig({
  site: 'https://schmelczer.dev',
  trailingSlash: 'ignore',
  integrations: [
    sitemap({
      filter: (page) => {
        const path = new URL(page).pathname;
        return !/^\/tags\/[^/]+\/?$/.test(path) && path !== '/404/';
      },
      serialize(item) {
        const url = new URL(item.url);
        const match = url.pathname.match(/^\/articles\/([^/]+)\/?$/);
        let lastmod = item.lastmod;
        if (match) {
          const date = postLastmodLookup.get(match[1]);
          if (date instanceof Date && !Number.isNaN(date.valueOf())) {
            lastmod = date.toISOString();
          }
        }
        return { ...item, changefreq: 'monthly', ...(lastmod ? { lastmod } : {}) };
      },
    }),
  ],
  image: {
    service: { entrypoint: 'astro/assets/services/sharp' },
    // SVG sources in src/content/**/_assets are author-controlled.
    dangerouslyProcessSVG: true,
  },
  vite: {
    // Pre-bundle the analytics tracker during dev server startup. It is only
    // referenced from a client `<script>`, so Vite would otherwise discover it
    // lazily on the first page request, re-optimize, and trigger a full reload.
    // That reload makes already-optimized deps (including the dev toolbar's
    // runtime) return 504 "Outdated Optimize Dep", which breaks the Astro Dev
    // Toolbar that the audit relies on. Listing it here optimizes it up front
    // so the first navigation is stable.
    optimizeDeps: {
      include: ['@plausible-analytics/tracker'],
    },
    server: {
      watch: {
        // Avoid inotify instance limits in dev containers and mounted volumes.
        usePolling: true,
      },
    },
  },
  markdown: {
    shikiConfig: {
      themes: {
        light: 'github-light',
        dark: 'github-dark',
      },
      defaultColor: false,
      wrap: false,
    },
    rehypePlugins: [
      rehypeSlug,
      [
        rehypeAutolinkHeadings,
        {
          behavior: 'append',
          properties: {
            className: ['heading-anchor'],
          },
          // Glyph rendered via CSS ::before so it doesn't leak into the TOC
          // when astro:content extracts heading.text from the rendered HTML.
          content: [],
        },
      ],
      // Make scrollable code blocks and tables reachable via keyboard (WCAG
      // 2.1.1): without tabindex, a keyboard user cannot focus a horizontally
      // overflowing <pre> or <table> to scroll it. tabindex=0 is sufficient
      // on its own; role=region would require a meaningful per-block label,
      // which we don't have at markdown level.
      function rehypeFocusableScrollables() {
        const SCROLLABLE = new Set(['pre', 'table']);
        return (tree) => {
          visit(tree, 'element', (node) => {
            if (!SCROLLABLE.has(node.tagName)) return;
            node.properties.tabindex = '0';
          });
        };
      },
      function rehypeLabelHeadingPermalinks() {
        function textOf(node) {
          if (!node) return '';
          if (node.type === 'text') return node.value ?? '';
          return (node.children ?? []).map(textOf).join('');
        }

        return (tree) => {
          visit(tree, 'element', (node) => {
            if (!/^h[2-6]$/.test(node.tagName)) return;
            const headingText = textOf(node).trim();
            if (!headingText) return;

            for (const child of node.children ?? []) {
              const className = child.properties?.className;
              const classes = Array.isArray(className) ? className : [className];
              if (child.tagName === 'a' && classes.includes('heading-anchor')) {
                child.properties.ariaLabel = `Permalink to ${headingText}`;
              }
            }
          });
        };
      },
    ],
  },
});
