import { Router, Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import { prisma } from '../db/prisma';
import { requireAuth, requires } from '../middleware/auth';
import { authService, type ResolvedAccess } from '../services/auth';
import { looksLikeApiKey, resolveApiKey } from '../services/apiKeys';
import { nostrService } from '../services/nostr';
import { importPostFromNostr } from '../services/postImport';

const JWT_SECRET = process.env.JWT_SECRET || 'change-me-in-production';

const router = Router();

// Resolves access for an optional Bearer token (JWT or API key) without
// rejecting unauthenticated callers, so listing endpoints can stay public while
// still recognizing staff who pass `?all=true`.
async function resolveOptionalAccess(req: Request): Promise<ResolvedAccess | null> {
  const header = req.headers.authorization;
  if (!header || !header.startsWith('Bearer ')) return null;
  const token = header.slice(7);
  try {
    if (looksLikeApiKey(token)) {
      return await resolveApiKey(token);
    }
    const payload = jwt.verify(token, JWT_SECRET) as { pubkey: string };
    return await authService.resolveAccess(payload.pubkey);
  } catch {
    return null;
  }
}

// Returns true when the requester is authenticated and may see hidden posts.
async function canViewHiddenPosts(req: Request): Promise<boolean> {
  const access = await resolveOptionalAccess(req);
  return !!access && (access.isSuperAdmin || access.permissions.has('blog.draft'));
}

// Resolves a blog slug to the Nostr event id used for reactions/replies,
// whether it's an indexed post or a live NIP-19 reference. Returns null if
// neither resolves.
async function resolveEventIdForSlug(slug: string): Promise<string | null> {
  const post = await prisma.post.findUnique({ where: { slug } });
  if (post) return post.nostrEventId;
  const event = await nostrService.resolveEventByIdentifier(slug);
  return event?.id ?? null;
}

function tagValue(event: { tags?: string[][] }, name: string): string | undefined {
  return event.tags?.find((t) => t[0] === name)?.[1];
}

// Shapes a live Nostr longform event into the same JSON the frontend expects
// from an indexed Post, so naddr/nevent/note links render through the regular
// post template instead of 404ing. `identifier` is the original URL segment and
// becomes the slug so canonical URLs and reaction/reply lookups stay stable.
function buildPostShapeFromEvent(
  event: { id: string; pubkey: string; content: string; created_at: number; tags?: string[][] },
  identifier: string
) {
  const title = tagValue(event, 'title') || 'Untitled';
  const summary = tagValue(event, 'summary') || null;
  const image = tagValue(event, 'image') || null;
  const publishedAtSec = Number(tagValue(event, 'published_at')) || event.created_at;
  const categories = (event.tags || [])
    .filter((t) => t[0] === 't' && t[1])
    .map((t) => ({ category: { id: t[1], name: t[1], slug: t[1] } }));

  return {
    id: event.id,
    nostrEventId: event.id,
    naddr: identifier.startsWith('naddr') ? identifier : null,
    title,
    slug: identifier,
    content: event.content || '',
    excerpt: summary,
    image,
    authorPubkey: event.pubkey,
    authorName: null,
    featured: false,
    visible: true,
    publishedAt: new Date(publishedAtSec * 1000).toISOString(),
    createdAt: new Date(event.created_at * 1000).toISOString(),
    categories,
  };
}

router.get('/', async (req: Request, res: Response) => {
  try {
    const page = parseInt(req.query.page as string) || 1;
    const limit = parseInt(req.query.limit as string) || 20;
    const category = req.query.category as string | undefined;
    const skip = (page - 1) * limit;

    const includeHidden = req.query.all === 'true' && (await canViewHiddenPosts(req));

    const where: any = {};
    if (!includeHidden) where.visible = true;
    if (category) {
      where.categories = {
        some: { category: { slug: category } },
      };
    }

    const [posts, total] = await Promise.all([
      prisma.post.findMany({
        where,
        include: {
          categories: { include: { category: true } },
        },
        orderBy: { publishedAt: 'desc' },
        skip,
        take: limit,
      }),
      prisma.post.count({ where }),
    ]);

    res.json({
      posts,
      total,
      pagination: { page, limit, total, pages: Math.ceil(total / limit) },
    });
  } catch (err) {
    console.error('List posts error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

router.get('/:slug', async (req: Request, res: Response) => {
  try {
    const slug = req.params.slug as string;
    const post = await prisma.post.findUnique({
      where: { slug },
      include: {
        categories: { include: { category: true } },
      },
    });

    if (post) {
      // Indexed posts store an empty body (the canonical copy lives on Nostr);
      // the frontend hydrates the body live from relays. Return as-is.
      res.json(post);
      return;
    }

    // Not indexed: resolve the slug as a live NIP-19 reference so the page has
    // real metadata (title, summary, image) for SEO instead of "Post Not
    // Found". The frontend still fetches the body from relays for display.
    const event = await nostrService.resolveEventByIdentifier(slug);
    if (event) {
      res.json(buildPostShapeFromEvent(event, slug));
      return;
    }

    res.status(404).json({ error: 'Post not found' });
  } catch (err) {
    console.error('Get post error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

router.post(
  '/import',
  requireAuth,
  requires('blog.draft'),
  async (req: Request, res: Response) => {
    try {
      const { nostrEventId, naddr, title, excerpt, authorPubkey, publishedAt, tags } = req.body;

      if (!nostrEventId || !title || !authorPubkey) {
        res.status(400).json({ error: 'nostrEventId, title, and authorPubkey are required' });
        return;
      }

      const result = await importPostFromNostr({
        nostrEventId,
        naddr,
        title,
        excerpt,
        authorPubkey,
        publishedAt,
        tags,
      });

      res.json(result);
    } catch (err) {
      console.error('Import post error:', err);
      res.status(500).json({ error: 'Internal server error' });
    }
  }
);

router.patch(
  '/:id',
  requireAuth,
  requires('blog.draft'),
  async (req: Request, res: Response) => {
    try {
      const { title, slug, excerpt, featured, visible, categories } = req.body;

      const post = await prisma.post.findUnique({ where: { id: req.params.id as string } });
      if (!post) {
        res.status(404).json({ error: 'Post not found' });
        return;
      }

      const updateData: any = {};
      if (title !== undefined) updateData.title = title;
      if (slug !== undefined) updateData.slug = slug;
      if (excerpt !== undefined) updateData.excerpt = excerpt;
      if (featured !== undefined) updateData.featured = featured;
      if (visible !== undefined) updateData.visible = visible;

      const updated = await prisma.post.update({
        where: { id: req.params.id as string },
        data: updateData,
      });

      if (categories && Array.isArray(categories)) {
        await prisma.postCategory.deleteMany({
          where: { postId: post.id },
        });
        await prisma.postCategory.createMany({
          data: categories.map((categoryId: string) => ({
            postId: post.id,
            categoryId,
          })),
        });
      }

      const result = await prisma.post.findUnique({
        where: { id: updated.id },
        include: { categories: { include: { category: true } } },
      });

      res.json(result);
    } catch (err) {
      console.error('Update post error:', err);
      res.status(500).json({ error: 'Internal server error' });
    }
  }
);

router.get('/:slug/reactions', async (req: Request, res: Response) => {
  try {
    const slug = req.params.slug as string;
    const eventId = await resolveEventIdForSlug(slug);
    if (!eventId) {
      res.status(404).json({ error: 'Post not found' });
      return;
    }

    const reactions = await nostrService.fetchReactions(eventId);
    res.json({ count: reactions.length, reactions });
  } catch (err) {
    console.error('Get reactions error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

router.get('/:slug/replies', async (req: Request, res: Response) => {
  try {
    const slug = req.params.slug as string;
    const eventId = await resolveEventIdForSlug(slug);
    if (!eventId) {
      res.status(404).json({ error: 'Post not found' });
      return;
    }

    const [replies, hiddenContent, blockedPubkeys] = await Promise.all([
      nostrService.fetchReplies(eventId),
      prisma.hiddenContent.findMany({ select: { nostrEventId: true } }),
      prisma.blockedPubkey.findMany({ select: { pubkey: true } }),
    ]);

    const hiddenIds = new Set(hiddenContent.map((h) => h.nostrEventId));
    const blockedKeys = new Set(blockedPubkeys.map((b) => b.pubkey));

    const filtered = replies.filter(
      (r) => !hiddenIds.has(r.id) && !blockedKeys.has(r.pubkey)
    );

    res.json({ count: filtered.length, replies: filtered });
  } catch (err) {
    console.error('Get replies error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

router.delete(
  '/:id',
  requireAuth,
  requires('blog.delete'),
  async (req: Request, res: Response) => {
    try {
      const post = await prisma.post.findUnique({ where: { id: req.params.id as string } });
      if (!post) {
        res.status(404).json({ error: 'Post not found' });
        return;
      }

      await prisma.post.delete({ where: { id: req.params.id as string } });
      res.json({ success: true });
    } catch (err) {
      console.error('Delete post error:', err);
      res.status(500).json({ error: 'Internal server error' });
    }
  }
);

export default router;
