import type { Metadata } from "next";
import { cookies } from "next/headers";
import { notFound } from "next/navigation";

import { API_BASE_URL, API_ENDPOINTS } from "@/api/constant";
import { IAPISuccesResponse } from "@/api/types/common";
import {
  IBlogPost,
  IBlogPostDetail,
  IBlogPostsResponseData,
} from "@/api/types/blog";

import BlogDetailContent from "./BlogDetailContent";

interface PageProps {
  params: Promise<{ slug: string }>;
}

const SITE_URL = (
  process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000"
).replace(/\/$/, "");
const ACCESS_TOKEN_COOKIE = "ms_access_token";
const REFRESH_TOKEN_COOKIE = "ms_refresh_token";

const getApiHeaders = async (
  includeAuth = false,
): Promise<Record<string, string>> => {
  const headers: Record<string, string> = {
    accept: "application/json",
    language: "en",
    timezone: "UTC",
    account: "default-key",
  };

  if (includeAuth) {
    const token = (await cookies()).get(ACCESS_TOKEN_COOKIE)?.value;
    if (token) headers.Authorization = `Bearer ${token}`;
  }

  return headers;
};

// Exchanges a refresh token for a fresh access token. Returns null if the
// refresh token is itself expired/invalid (caller then falls back to no token).
async function refreshAccessToken(
  refreshToken: string,
): Promise<string | null> {
  try {
    const headers = await getApiHeaders();
    const res = await fetch(`${API_BASE_URL}${API_ENDPOINTS.refreshToken}`, {
      method: "POST",
      headers: { ...headers, "content-type": "application/json" },
      body: JSON.stringify({ refreshToken }),
      cache: "no-store",
    });
    if (!res.ok) return null;
    const json = (await res.json()) as IAPISuccesResponse<{
      token: string;
      refreshToken: string;
    }>;
    return json.responseData?.token ?? null;
  } catch {
    return null;
  }
}

const stripHtml = (html: string): string =>
  html
    .replace(/<[^>]*>/g, " ")
    .replace(/\s+/g, " ")
    .trim();

const buildCanonical = (post: IBlogPostDetail, slug: string): string => {
  const raw = post.canonicalUrl?.trim();
  if (raw && /^https?:\/\//i.test(raw)) return raw;
  const path = raw && !raw.includes("/") ? raw : slug;
  return `${SITE_URL}/blog/detail/${path}`;
};

// Performs the blog-post request with an explicit bearer token (or none).
// Authenticated requests bypass the cache; anonymous ones are revalidated.
async function requestPost(
  slug: string,
  token?: string | null,
): Promise<Response> {
  const headers = await getApiHeaders();
  if (token) headers.Authorization = `Bearer ${token}`;
  const url = `${API_BASE_URL}${API_ENDPOINTS.blogPostByCode}/${encodeURIComponent(slug)}`;
  return token
    ? fetch(url, { headers, cache: "no-store" })
    : fetch(url, {
        headers,
        next: { revalidate: 60, tags: [`blog-post:${slug}`] },
      });
}

async function fetchPost(
  slug: string,
  { includeAuth = false }: { includeAuth?: boolean } = {},
): Promise<IBlogPostDetail | null> {
  try {
    const cookieStore = await cookies();
    const refreshToken = includeAuth
      ? cookieStore.get(REFRESH_TOKEN_COOKIE)?.value
      : undefined;
    let accessToken = includeAuth
      ? cookieStore.get(ACCESS_TOKEN_COOKIE)?.value
      : undefined;

    // A logged-in user (refresh token present) must always send a token. If the
    // access token cookie is missing/expired, refresh it first — otherwise the
    // request would silently fall back to the anonymous version even though the
    // session is still valid.
    if (includeAuth && !accessToken && refreshToken) {
      accessToken = (await refreshAccessToken(refreshToken)) ?? undefined;
    }

    let res = await requestPost(slug, accessToken);

    // Token was sent but rejected as expired → refresh and retry once. If the
    // refresh token is also dead (null) we fall back to no token (public view).
    if (res.status === 401 && includeAuth && refreshToken) {
      const newToken = await refreshAccessToken(refreshToken);
      res = await requestPost(slug, newToken);
    }

    if (!res.ok) return null;
    const json = (await res.json()) as IAPISuccesResponse<IBlogPostDetail>;
    return json.responseData ?? null;
  } catch {
    return null;
  }
}

async function fetchMorePosts(categoryIds?: number[]): Promise<IBlogPost[]> {
  try {
    const params = new URLSearchParams({
      page: "1",
      perPage: "6",
      sortBy: "id",
      sortDirection: "desc",
    });
    // Repeat the param for each category → categories=1&categories=2
    categoryIds?.forEach((id) => params.append("categories", String(id)));
    const headers = await getApiHeaders();

    const res = await fetch(
      `${API_BASE_URL}${API_ENDPOINTS.blogPosts}?${params.toString()}`,
      { headers, next: { revalidate: 60, tags: ["blog-posts:list"] } },
    );
    if (!res.ok) return [];
    const json =
      (await res.json()) as IAPISuccesResponse<IBlogPostsResponseData>;
    return json.responseData?.data ?? [];
  } catch {
    return [];
  }
}

export async function generateMetadata({
  params,
}: PageProps): Promise<Metadata> {
  const { slug } = await params;
  const post = await fetchPost(slug);

  if (!post) {
    return { title: "Blog post not found" };
  }

  const title = post.metaTitle?.trim() || post.title;
  const previewText = stripHtml(post.limitedContent ?? post.description ?? "");
  const description =
    post.metaDescription?.trim() ||
    post.exerpt?.trim() ||
    (previewText ? previewText.slice(0, 160) : undefined);
  const image = post.featuredImage?.filePath || undefined;
  const authors = post.author?.name ? [{ name: post.author.name }] : undefined;
  const tags = post.postTags?.map((t) => t.name).filter(Boolean);
  const canonical = buildCanonical(post, slug);
  const isPublic = post.visibility === "public";

  return {
    title,
    description,
    keywords: post.metaKeywords?.trim() || undefined,
    authors,
    alternates: { canonical },
    robots: isPublic ? undefined : { index: false, follow: false },
    openGraph: {
      title,
      description,
      type: "article",
      url: canonical,
      publishedTime: post.publishedAt,
      modifiedTime: post.updatedAt,
      authors: post.author?.name ? [post.author.name] : undefined,
      tags,
      images: image ? [{ url: image }] : undefined,
    },
    twitter: {
      card: "summary_large_image",
      title,
      description,
      images: image ? [image] : undefined,
    },
  };
}

const BlogDetailsPage = async ({ params }: PageProps) => {
  const { slug } = await params;
  const post = await fetchPost(slug, { includeAuth: true });

  if (!post) {
    notFound();
  }

  const morePosts = await fetchMorePosts(post.postCategories?.map((c) => c.id));

  return <BlogDetailContent post={post} morePostsRaw={morePosts} />;
};

export default BlogDetailsPage;
