import { lazy, Suspense, useEffect, useMemo, useRef, useState } from "react";
import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ExternalLink, Save, Eye, Trash2, ChevronLeft } from "lucide-react";
import { toast } from "sonner";

import { authStore } from "@/lib/auth";
import {
  adminBlogApi,
  adminBlogQueryKeys,
  type AdminPost,
} from "@/lib/admin-blog";
import { Button, Card, cn } from "@rubis/ui";
import { HeroImageUpload } from "@/components/admin/HeroImageUpload";

// Lazy : @uiw/react-md-editor charge ~250 KB d'éditeur + preview, on ne veut
// pas le bundler dans le critical path du SaaS. Code-splitté par route.
const MdEditor = lazy(() => import("@uiw/react-md-editor"));

export const Route = createFileRoute("/_app/admin/blog_/$id")({
  beforeLoad: () => {
    if (!authStore.user?.isAdmin) {
      throw redirect({ to: "/" });
    }
  },
  loader: ({ params, context }) => {
    void context.queryClient.prefetchQuery({
      queryKey: adminBlogQueryKeys.detail(params.id),
      queryFn: () => adminBlogApi.get(params.id),
    });
  },
  component: AdminBlogEditor,
});

/* ============================================================================
 * Validations SEO miroir des règles backend (cf. apps/api/app/validators/post)
 * — on les exécute ici pour bloquer le bouton "Publier" avec feedback live.
 * ========================================================================= */

const VALIDATION_RULES = {
  titleMaxChars: 60,
  excerptMinChars: 120,
  excerptMaxChars: 160,
  minWordCount: 600,
} as const;

type ValidationIssue = { field: string; message: string };

function validateForPublish(
  draft: { title: string; excerpt: string; heroImageUrl: string | null; heroImageAlt: string | null; wordCount: number; slug: string },
): ValidationIssue[] {
  const issues: ValidationIssue[] = [];
  if (!draft.slug || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(draft.slug)) {
    issues.push({ field: "slug", message: "Slug invalide (kebab-case ASCII)." });
  }
  if (draft.title.length > VALIDATION_RULES.titleMaxChars) {
    issues.push({
      field: "title",
      message: `Titre trop long pour Google (${draft.title.length} > ${VALIDATION_RULES.titleMaxChars}).`,
    });
  }
  if (draft.title.length < 5) {
    issues.push({ field: "title", message: "Titre trop court (min 5 chars)." });
  }
  if (draft.excerpt.length < VALIDATION_RULES.excerptMinChars) {
    issues.push({
      field: "excerpt",
      message: `Excerpt trop court (${draft.excerpt.length} < ${VALIDATION_RULES.excerptMinChars}).`,
    });
  }
  if (draft.excerpt.length > VALIDATION_RULES.excerptMaxChars) {
    issues.push({
      field: "excerpt",
      message: `Excerpt trop long (${draft.excerpt.length} > ${VALIDATION_RULES.excerptMaxChars}).`,
    });
  }
  if (!draft.heroImageUrl) {
    issues.push({ field: "heroImageUrl", message: "Hero image requise." });
  }
  if (!draft.heroImageAlt || draft.heroImageAlt.length < 3) {
    issues.push({ field: "heroImageAlt", message: "Texte alt requis." });
  }
  if (draft.wordCount < VALIDATION_RULES.minWordCount) {
    issues.push({
      field: "contentMd",
      message: `Article trop court (${draft.wordCount} < ${VALIDATION_RULES.minWordCount} mots).`,
    });
  }
  return issues;
}

/* ============================================================================
 * Component
 * ========================================================================= */

function AdminBlogEditor() {
  const { id } = Route.useParams();
  const navigate = useNavigate();

  const { data: post, isPending } = useQuery({
    queryKey: adminBlogQueryKeys.detail(id),
    queryFn: () => adminBlogApi.get(id),
  });

  if (isPending || !post) {
    return (
      <div className="container mx-auto max-w-[1100px] px-5 py-8 text-ink-3">
        Chargement…
      </div>
    );
  }

  return <Editor post={post} onBack={() => navigate({ to: "/admin/blog" })} />;
}

function Editor({ post, onBack }: { post: AdminPost; onBack: () => void }) {
  const qc = useQueryClient();

  // État local — on travaille sur une copie du post pour autosave debouncée.
  const [draft, setDraft] = useState({
    slug: post.slug ?? "",
    title: post.title ?? "",
    excerpt: post.excerpt ?? "",
    contentMd: post.contentMd ?? "",
    heroImageUrl: post.heroImageUrl,
    heroImageAlt: post.heroImageAlt,
    tags: (post.tags ?? []).join(", "),
  });

  // Re-sync si le post est rafraîchi par un autre canal (rare mais sûr).
  useEffect(() => {
    setDraft({
      slug: post.slug ?? "",
      title: post.title ?? "",
      excerpt: post.excerpt ?? "",
      contentMd: post.contentMd ?? "",
      heroImageUrl: post.heroImageUrl,
      heroImageAlt: post.heroImageAlt,
      tags: (post.tags ?? []).join(", "),
    });
  }, [post.id]); // eslint-disable-line react-hooks/exhaustive-deps

  const wordCount = useMemo(() => countWords(draft.contentMd), [draft.contentMd]);
  const issues = useMemo(
    () =>
      validateForPublish({
        title: draft.title,
        excerpt: draft.excerpt,
        slug: draft.slug,
        heroImageUrl: draft.heroImageUrl,
        heroImageAlt: draft.heroImageAlt,
        wordCount,
      }),
    [draft, wordCount],
  );

  /* === Save mutation (manuel + autosave debounce) === */
  const saveMut = useMutation({
    mutationFn: () =>
      adminBlogApi.update(post.id, {
        slug: draft.slug,
        title: draft.title,
        excerpt: draft.excerpt,
        contentMd: draft.contentMd,
        heroImageUrl: draft.heroImageUrl,
        heroImageAlt: draft.heroImageAlt,
        tags: draft.tags
          .split(",")
          .map((t) => t.trim())
          .filter((t) => t.length > 0),
      }),
    onSuccess: (saved) => {
      qc.setQueryData(adminBlogQueryKeys.detail(post.id), saved);
      qc.invalidateQueries({ queryKey: adminBlogQueryKeys.list() });
    },
  });

  // Autosave debounce 2s sur changement de draft.
  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  useEffect(() => {
    if (debounceRef.current) clearTimeout(debounceRef.current);
    debounceRef.current = setTimeout(() => {
      // Skip si rien n'a changé vs persisted
      const persisted = {
        slug: post.slug,
        title: post.title,
        excerpt: post.excerpt,
        contentMd: post.contentMd,
        heroImageUrl: post.heroImageUrl,
        heroImageAlt: post.heroImageAlt,
        tags: post.tags.join(", "),
      };
      const changed = JSON.stringify(persisted) !== JSON.stringify(draft);
      if (changed && !saveMut.isPending) {
        saveMut.mutate();
      }
    }, 2000);
    return () => {
      if (debounceRef.current) clearTimeout(debounceRef.current);
    };
  }, [draft]); // eslint-disable-line react-hooks/exhaustive-deps

  /* === Publish === */
  const publishMut = useMutation({
    mutationFn: async () => {
      // S'assurer qu'on a sauvé avant de publier (sinon l'API juge sur l'état persisté).
      await saveMut.mutateAsync();
      return adminBlogApi.publish(post.id);
    },
    onSuccess: (saved) => {
      qc.setQueryData(adminBlogQueryKeys.detail(post.id), saved);
      qc.invalidateQueries({ queryKey: adminBlogQueryKeys.list() });
      toast.success("Article publié — visible sur rubis.pro/blog (cache 5 min).");
    },
    onError: (err: Error) => {
      toast.error(`Échec publication : ${err.message}`);
    },
  });

  const unpublishMut = useMutation({
    mutationFn: () => adminBlogApi.unpublish(post.id),
    onSuccess: (saved) => {
      qc.setQueryData(adminBlogQueryKeys.detail(post.id), saved);
      qc.invalidateQueries({ queryKey: adminBlogQueryKeys.list() });
      toast.success("Article repassé en brouillon.");
    },
  });

  const deleteMut = useMutation({
    mutationFn: () => adminBlogApi.delete(post.id),
    onSuccess: () => {
      toast.success("Article supprimé.");
      onBack();
    },
  });

  const canPublish = issues.length === 0;
  const isPublished = post.status === "published";
  const publicUrl = `https://rubis.pro/blog/${post.slug}`;

  return (
    <div className="container mx-auto max-w-[1280px] px-5 py-6">
      {/* Topbar */}
      <div className="flex items-center justify-between mb-6 flex-wrap gap-4">
        <button
          type="button"
          onClick={onBack}
          className="inline-flex items-center gap-1 text-[14px] text-ink-2 hover:text-rubis"
        >
          <ChevronLeft size={16} />
          Retour aux articles
        </button>

        <div className="flex items-center gap-2 flex-wrap">
          <SaveStatus
            isSaving={saveMut.isPending}
            lastSavedAt={saveMut.data?.updatedAt ?? post.updatedAt}
          />
          {isPublished && (
            <a
              href={publicUrl}
              target="_blank"
              rel="noopener"
              className="inline-flex items-center gap-1.5 text-[13px] text-ink-2 hover:text-rubis px-3 py-2"
            >
              <ExternalLink size={14} />
              Voir sur rubis.pro
            </a>
          )}
          <Button
            type="button"
            variant="ghost"
            size="sm"
            onClick={() => saveMut.mutate()}
            loading={saveMut.isPending}
          >
            <Save size={14} />
            Sauvegarder
          </Button>
          {isPublished ? (
            <Button
              type="button"
              variant="secondary"
              onClick={() => unpublishMut.mutate()}
              loading={unpublishMut.isPending}
            >
              Dépublier
            </Button>
          ) : (
            <Button
              type="button"
              onClick={() => publishMut.mutate()}
              disabled={!canPublish}
              loading={publishMut.isPending}
              title={canPublish ? "Publier sur rubis.pro/blog" : "Corrige les erreurs SEO d'abord"}
            >
              <Eye size={14} />
              Publier
            </Button>
          )}
          <Button
            type="button"
            variant="ghost"
            size="sm"
            onClick={() => {
              if (confirm(`Supprimer définitivement "${post.title}" ?`)) {
                deleteMut.mutate();
              }
            }}
          >
            <Trash2 size={14} />
          </Button>
        </div>
      </div>

      {/* Title */}
      <input
        type="text"
        value={draft.title}
        onChange={(e) => setDraft((d) => ({ ...d, title: e.target.value }))}
        placeholder="Titre de l'article…"
        className="w-full bg-transparent border-none font-display text-[32px] sm:text-[40px] font-bold tracking-[-0.025em] text-ink placeholder:text-ink-3 focus:outline-none mb-2"
      />
      <CharCounter
        value={draft.title.length}
        max={VALIDATION_RULES.titleMaxChars}
        label="Titre"
      />

      {/* Slug */}
      <div className="mt-3 flex items-center gap-2 text-[13px] text-ink-3">
        <span>rubis.pro/blog/</span>
        <input
          type="text"
          value={draft.slug}
          onChange={(e) => setDraft((d) => ({ ...d, slug: e.target.value }))}
          placeholder="slug-de-l-article"
          className="flex-1 max-w-[400px] bg-transparent border-b border-line font-mono text-ink focus:outline-none focus:border-rubis"
        />
        <button
          type="button"
          onClick={async () => {
            try {
              const result = await adminBlogApi.suggestSlug(draft.title);
              setDraft((d) => ({ ...d, slug: result.slug }));
            } catch {
              /* swallow */
            }
          }}
          className="text-rubis hover:text-rubis-deep underline-offset-2 hover:underline"
        >
          générer depuis le titre
        </button>
      </div>

      {/* Layout 2 colonnes */}
      <div className="mt-8 grid lg:grid-cols-[2fr_1fr] gap-8">
        {/* Colonne gauche : éditeur markdown */}
        <div data-color-mode="light" className="rubis-md-editor-wrapper">
          <Suspense fallback={<div className="text-ink-3">Chargement de l'éditeur…</div>}>
            <MdEditor
              value={draft.contentMd}
              onChange={(val) => setDraft((d) => ({ ...d, contentMd: val ?? "" }))}
              height={650}
              preview="live"
              previewOptions={{
                /* On utilise le preview natif @uiw, qui rend correctement le GFM
                   markdown pour donner un aperçu. Le rendu final sur rubis.pro
                   passe par notre blog_renderer (marked v15), légèrement
                   différent — l'aperçu sert à valider la STRUCTURE, pas le
                   pixel-perfect. Pour ça : "Voir sur rubis.pro" après publish. */
              }}
            />
          </Suspense>
          <div className="mt-3 text-[12.5px] text-ink-3 flex items-center gap-3">
            <span>{wordCount} mots · ~{Math.max(1, Math.round(wordCount / 220))} min</span>
            {wordCount < VALIDATION_RULES.minWordCount && (
              <span className="text-rubis-deep">
                · Min {VALIDATION_RULES.minWordCount} mots requis pour publier
              </span>
            )}
          </div>
        </div>

        {/* Colonne droite : meta SEO + hero */}
        <div className="space-y-6">
          {/* Hero image */}
          <Card padding="md">
            <div className="font-display font-semibold text-[14px] uppercase tracking-[0.08em] text-ink mb-3">
              Image de couverture
            </div>
            <HeroImageUpload
              value={draft.heroImageUrl}
              alt={draft.heroImageAlt}
              onChange={({ url, alt }) =>
                setDraft((d) => ({ ...d, heroImageUrl: url, heroImageAlt: alt }))
              }
            />
          </Card>

          {/* Meta description */}
          <Card padding="md">
            <div className="font-display font-semibold text-[14px] uppercase tracking-[0.08em] text-ink mb-3">
              Description (SEO)
            </div>
            <textarea
              value={draft.excerpt}
              onChange={(e) => setDraft((d) => ({ ...d, excerpt: e.target.value }))}
              rows={4}
              placeholder="120 à 160 caractères. Cette ligne apparaît dans les résultats Google et sur LinkedIn/Twitter quand l'article est partagé."
              className="w-full px-3 py-2 rounded-default border border-line bg-white text-[14px] text-ink leading-relaxed focus:border-rubis focus:outline-none focus:ring-4 focus:ring-rubis-glow/40 resize-none"
            />
            <div className="text-[11.5px] mt-1.5 flex items-center justify-between">
              <span
                className={cn(
                  "tabular-nums",
                  draft.excerpt.length < VALIDATION_RULES.excerptMinChars ||
                    draft.excerpt.length > VALIDATION_RULES.excerptMaxChars
                    ? "text-rubis-deep"
                    : "text-ink-3",
                )}
              >
                {draft.excerpt.length} / {VALIDATION_RULES.excerptMinChars}-
                {VALIDATION_RULES.excerptMaxChars} chars
              </span>
            </div>
          </Card>

          {/* Tags */}
          <Card padding="md">
            <div className="font-display font-semibold text-[14px] uppercase tracking-[0.08em] text-ink mb-3">
              Tags
            </div>
            <input
              type="text"
              value={draft.tags}
              onChange={(e) => setDraft((d) => ({ ...d, tags: e.target.value }))}
              placeholder="trésorerie, LME, retards (séparés par virgules)"
              className="w-full px-3 py-2 rounded-default border border-line bg-white text-[14px] text-ink focus:border-rubis focus:outline-none focus:ring-4 focus:ring-rubis-glow/40"
            />
            <div className="text-[11.5px] text-ink-3 mt-1.5">
              Sert au "Articles liés" sur rubis.pro/blog (intersection de tags). Max 5.
            </div>
          </Card>

          {/* Aperçu Google snippet */}
          <Card padding="md">
            <div className="font-display font-semibold text-[14px] uppercase tracking-[0.08em] text-ink mb-3">
              Aperçu Google
            </div>
            <div className="bg-white border border-line rounded-default p-4">
              <div className="text-[12.5px] text-ink-3">rubis.pro › blog › {draft.slug}</div>
              <div className="mt-1 font-display text-[18px] text-rubis hover:underline cursor-pointer">
                {(draft.title || "…").slice(0, 60)}
              </div>
              <div className="text-[13.5px] text-ink-2 leading-snug line-clamp-2 mt-1">
                {draft.excerpt || "Description…"}
              </div>
            </div>
          </Card>

          {/* Validations bloquantes */}
          {issues.length > 0 && (
            <Card padding="md" variant="flat">
              <div className="font-display font-semibold text-[14px] uppercase tracking-[0.08em] text-rubis-deep mb-2">
                À corriger avant publication
              </div>
              <ul className="space-y-1.5 text-[13px] text-ink-2">
                {issues.map((issue, i) => (
                  <li key={i} className="flex items-baseline gap-2">
                    <span aria-hidden className="size-1.5 rounded-full bg-rubis flex-shrink-0 mt-1.5" />
                    <span>{issue.message}</span>
                  </li>
                ))}
              </ul>
            </Card>
          )}
        </div>
      </div>
    </div>
  );
}

function SaveStatus({ isSaving, lastSavedAt }: { isSaving: boolean; lastSavedAt: string | null }) {
  if (isSaving) {
    return <span className="text-[12.5px] text-ink-3">Sauvegarde…</span>;
  }
  if (!lastSavedAt) return null;
  const date = new Date(lastSavedAt);
  return (
    <span className="text-[12.5px] text-ink-3" title={date.toISOString()}>
      Sauvé à{" "}
      {new Intl.DateTimeFormat("fr-FR", { hour: "2-digit", minute: "2-digit" }).format(date)}
    </span>
  );
}

function CharCounter({ value, max, label }: { value: number; max: number; label: string }) {
  const isOver = value > max;
  return (
    <div
      className={cn(
        "text-[11.5px] tabular-nums",
        isOver ? "text-rubis-deep" : "text-ink-3",
      )}
    >
      {label} : {value}/{max} chars {isOver && "· trop long pour Google"}
    </div>
  );
}

/** Compte les mots côté client — heuristique simple, le serveur a la source de vérité. */
function countWords(md: string | null | undefined): number {
  if (!md) return 0;
  const stripped = md
    .replace(/```[\s\S]*?```/g, " ")
    .replace(/`[^`]*`/g, " ")
    .replace(/!\[[^\]]*\]\([^)]*\)/g, " ")
    .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
    .replace(/[*_~#>]/g, " ");
  const matches = stripped.match(/\S+/g);
  return matches ? matches.length : 0;
}
