import { slugify } from "@/lib/slug";

// Pure and client-safe: the SEO page runs it on the server, the article
// editor runs it live in the browser.

/** Search results cut titles off at roughly this many characters. */
export const SEO_TITLE_MAX = 60;
export const META_DESCRIPTION_MIN = 70;
export const META_DESCRIPTION_MAX = 160;

/** Articles shorter than this rarely rank. */
const CONTENT_MIN_WORDS = 300;

export type SeoInput = {
  title: string;
  slug: string;
  excerpt: string;
  wordCount: number;
  seoTitle: string | null;
  metaDescription: string | null;
  focusKeyword: string | null;
  featuredImage: string | null;
  featuredImageAlt: string | null;
  robotsIndex: boolean;
  canonicalUrl: string | null;
};

export type SeoCheck = {
  id: string;
  label: string;
  status: "pass" | "warn" | "fail";
  message: string;
};

/**
 * How much each check counts towards the score. Titles, descriptions and
 * indexing matter most; the canonical check is informational only.
 */
const CHECK_WEIGHTS: Record<string, number> = {
  "seo-title": 2,
  "meta-description": 2,
  "meta-description-length": 1,
  "focus-keyword": 1,
  "keyword-in-title": 1,
  "keyword-in-description": 1,
  "keyword-in-slug": 1,
  "content-length": 1,
  "featured-image": 1,
  "featured-image-alt": 1,
  indexable: 2,
  canonical: 0,
};

const STATUS_POINTS: Record<SeoCheck["status"], number> = {
  pass: 1,
  warn: 0.5,
  fail: 0,
};

const normalize = (value: string) =>
  value.toLowerCase().replace(/\s+/g, " ").trim();

const characters = (count: number) =>
  `${count} character${count === 1 ? "" : "s"}`;

/**
 * Runs every on-page SEO check for an article. The list always contains the
 * same checks in the same order (plus `canonical` when one is set), so the
 * editor can render it without layout jumps.
 */
export function analyzeSeo(input: SeoInput): SeoCheck[] {
  const checks: SeoCheck[] = [];
  const title = (input.seoTitle?.trim() || input.title.trim()).replace(
    /\s+/g,
    " ",
  );
  const usesArticleTitle = !input.seoTitle?.trim();
  const metaDescription = input.metaDescription?.trim() ?? "";
  const description = (metaDescription || input.excerpt.trim()).replace(
    /\s+/g,
    " ",
  );
  const keywordText = input.focusKeyword?.trim() ?? "";
  const keyword = normalize(keywordText);

  // SEO title
  if (!title) {
    checks.push({
      id: "seo-title",
      label: "SEO title",
      status: "fail",
      message: "Add a title. It's the headline shown in search results.",
    });
  } else if (title.length > SEO_TITLE_MAX) {
    checks.push({
      id: "seo-title",
      label: "SEO title",
      status: "warn",
      message: `${characters(title.length)}. Search results cut titles off after about ${SEO_TITLE_MAX}.`,
    });
  } else {
    checks.push({
      id: "seo-title",
      label: "SEO title",
      status: "pass",
      message: usesArticleTitle
        ? `Uses the article title (${characters(title.length)}).`
        : `${characters(title.length)}.`,
    });
  }

  // Meta description
  if (metaDescription) {
    checks.push({
      id: "meta-description",
      label: "Meta description",
      status: "pass",
      message: "Set.",
    });
  } else if (description) {
    checks.push({
      id: "meta-description",
      label: "Meta description",
      status: "warn",
      message:
        "Not set, so search engines see the excerpt. Write one that sells the click.",
    });
  } else {
    checks.push({
      id: "meta-description",
      label: "Meta description",
      status: "fail",
      message:
        "Add a meta description. Without one, search engines pick a random snippet.",
    });
  }

  if (!description) {
    checks.push({
      id: "meta-description-length",
      label: "Description length",
      status: "warn",
      message: `Aim for ${META_DESCRIPTION_MIN}–${META_DESCRIPTION_MAX} characters.`,
    });
  } else if (description.length < META_DESCRIPTION_MIN) {
    checks.push({
      id: "meta-description-length",
      label: "Description length",
      status: "warn",
      message: `${characters(description.length)}. Aim for ${META_DESCRIPTION_MIN}–${META_DESCRIPTION_MAX}.`,
    });
  } else if (description.length > META_DESCRIPTION_MAX) {
    checks.push({
      id: "meta-description-length",
      label: "Description length",
      status: "warn",
      message: `${characters(description.length)}. Search results cut descriptions off after about ${META_DESCRIPTION_MAX}.`,
    });
  } else {
    checks.push({
      id: "meta-description-length",
      label: "Description length",
      status: "pass",
      message: `${characters(description.length)}.`,
    });
  }

  // Focus keyword and where it appears
  checks.push(
    keyword
      ? {
          id: "focus-keyword",
          label: "Focus keyword",
          status: "pass",
          message: `“${keywordText}”.`,
        }
      : {
          id: "focus-keyword",
          label: "Focus keyword",
          status: "warn",
          message:
            "Set the phrase readers search for, to check the title, description and URL.",
        },
  );

  const placement = (
    id: string,
    label: string,
    where: string,
    found: boolean,
  ): SeoCheck => {
    if (!keyword) {
      return {
        id,
        label,
        status: "warn",
        message: "Set a focus keyword to check this.",
      };
    }
    return found
      ? {
          id,
          label,
          status: "pass",
          message: `The focus keyword is in the ${where}.`,
        }
      : {
          id,
          label,
          status: "warn",
          message: `Use the focus keyword in the ${where}.`,
        };
  };

  const keywordSlug = slugify(keyword);
  checks.push(
    placement(
      "keyword-in-title",
      "Keyword in title",
      "SEO title",
      Boolean(keyword) && normalize(title).includes(keyword),
    ),
    placement(
      "keyword-in-description",
      "Keyword in description",
      "meta description",
      Boolean(keyword) && normalize(description).includes(keyword),
    ),
    placement(
      "keyword-in-slug",
      "Keyword in URL",
      "URL slug",
      Boolean(keywordSlug) &&
        `-${input.slug.toLowerCase()}-`.includes(`-${keywordSlug}-`),
    ),
  );

  // Content
  const words = Math.max(0, Math.floor(input.wordCount));
  checks.push(
    words >= CONTENT_MIN_WORDS
      ? {
          id: "content-length",
          label: "Content length",
          status: "pass",
          message: `${words.toLocaleString("en-US")} words.`,
        }
      : {
          id: "content-length",
          label: "Content length",
          status: "warn",
          message: `${words.toLocaleString("en-US")} word${words === 1 ? "" : "s"}. Aim for at least ${CONTENT_MIN_WORDS}.`,
        },
  );

  // Featured image
  const hasImage = Boolean(input.featuredImage?.trim());
  checks.push(
    hasImage
      ? {
          id: "featured-image",
          label: "Featured image",
          status: "pass",
          message: "Set.",
        }
      : {
          id: "featured-image",
          label: "Featured image",
          status: "warn",
          message: "Add one. It's used in link previews and the blog listing.",
        },
  );
  checks.push(
    !hasImage
      ? {
          id: "featured-image-alt",
          label: "Image alt text",
          status: "warn",
          message: "Add a featured image, then describe it.",
        }
      : input.featuredImageAlt?.trim()
        ? {
            id: "featured-image-alt",
            label: "Image alt text",
            status: "pass",
            message: "Set.",
          }
        : {
            id: "featured-image-alt",
            label: "Image alt text",
            status: "warn",
            message:
              "Describe the featured image for screen readers and image search.",
          },
  );

  // Indexing
  checks.push(
    input.robotsIndex
      ? {
          id: "indexable",
          label: "Indexing",
          status: "pass",
          message: "Search engines can index this article.",
        }
      : {
          id: "indexable",
          label: "Indexing",
          status: "warn",
          message:
            "Hidden from search: “noindex” is set, so it won't appear in results.",
        },
  );

  const canonical = input.canonicalUrl?.trim();
  if (canonical) {
    checks.push({
      id: "canonical",
      label: "Canonical URL",
      status: "pass",
      message: `Points to ${canonical}. Search engines credit that URL instead of this article's own, if they differ.`,
    });
  }

  return checks;
}

/** 0–100. A pass earns full weight, a warning half, a failure nothing. */
export function seoScore(checks: SeoCheck[]): number {
  let earned = 0;
  let total = 0;
  for (const check of checks) {
    const weight = CHECK_WEIGHTS[check.id] ?? 1;
    total += weight;
    earned += weight * STATUS_POINTS[check.status];
  }
  return total === 0 ? 100 : Math.round((earned / total) * 100);
}
