import type {
  ActionFunctionArgs,
  LoaderFunctionArgs,
} from "@remix-run/node";
import { json } from "@remix-run/node";
import {
  useActionData,
  useLoaderData,
  useSubmit,
  useNavigation,
} from "@remix-run/react";
import {
  Page,
  Layout,
  Card,
  Tabs,
  BlockStack,
  InlineStack,
  InlineGrid,
  Text,
  Badge,
  TextField,
  Banner,
  Button,
  Box,
  Divider,
} from "@shopify/polaris";
import { useState, useCallback, useEffect, useRef } from "react";

import { authenticate, PLAN_STANDARD, PLAN_PRO } from "../shopify.server";
import { ensureShopSetup } from "../lib/shop-lifecycle.server";
import { syncRulesToMetafield } from "../lib/metafield-sync.server";
import { enqueueJob, JobType } from "../lib/jobs.server";
import {
  RULES,
  TAB_GROUPS,
  getRulesByTabGroup,
  type RuleDefinition,
  type RuleTier,
  type SerpPreview,
} from "../lib/rules";
import db from "../db.server";

const MAX_CUSTOM_VALUE_LENGTH = 2000;
// Threshold above which a CSV custom value is processed asynchronously via pg-boss.
// Small inputs keep the synchronous DB-then-metafield-sync path.
const ASYNC_CSV_CHAR_THRESHOLD = 500;
const ASYNC_CSV_ENTRY_THRESHOLD = 50;

function countCsvEntries(raw: string): number {
  if (!raw) return 0;
  let n = 0;
  for (const part of raw.split(/[,\n\r]+/)) {
    if (part.trim().length > 0) n++;
  }
  return n;
}

export const loader = async ({ request }: LoaderFunctionArgs) => {
  const { admin, session, billing } = await authenticate.admin(request);

  let shopTier: RuleTier = "pro"; // default: all unlocked in dev
  if (process.env.BILLING_ENABLED === "true") {
    const proCheck = await billing.check({ plans: [PLAN_PRO] });
    if (proCheck.hasActivePayment) {
      shopTier = "pro";
    } else {
      const standardCheck = await billing.check({ plans: [PLAN_STANDARD] });
      shopTier = standardCheck.hasActivePayment ? "standard" : "free";
    }
  }

  const shopDomain = await ensureShopSetup(admin, session);

  const shopRules = await db.shopRule.findMany({
    where: { shopId: shopDomain },
  });

  const shop = await db.shop.findUnique({
    where: { id: shopDomain },
  });

  const rulesState: Record<string, { enabled: boolean; customValue: string }> =
    {};
  for (const rule of RULES) {
    const shopRule = shopRules.find((sr) => sr.ruleKey === rule.key);
    rulesState[rule.key] = {
      enabled: shopRule?.enabled ?? rule.defaultEnabled,
      customValue: shopRule?.customValue ?? "",
    };
  }

  return json({
    shopDomain,
    rulesState,
    lastSyncedAt: shop?.lastSyncedAt?.toISOString() ?? null,
    shopTier,
  });
};

export const action = async ({ request }: ActionFunctionArgs) => {
  const { admin, session } = await authenticate.admin(request);
  const shopDomain = session.shop;
  const formData = await request.formData();

  const syncUpdates: { ruleKey: string; enabled: boolean; customValue?: string }[] =
    [];
  const asyncJobs: { ruleKey: string; jobId: string; ruleLabel: string }[] = [];

  for (const rule of RULES) {
    const enabled = formData.get(`rule_${rule.key}`) === "true";
    const rawCustomValue = rule.hasCustomValue
      ? ((formData.get(`value_${rule.key}`) as string) ?? "")
      : undefined;

    if (
      rawCustomValue !== undefined &&
      rawCustomValue.length > MAX_CUSTOM_VALUE_LENGTH
    ) {
      return json({
        success: false,
        error: `Le champ "${rule.customValueLabel ?? rule.label}" dépasse la limite de ${MAX_CUSTOM_VALUE_LENGTH} caractères.`,
        lastSyncedAt: null,
        asyncJobs: [] as { ruleKey: string; jobId: string; ruleLabel: string }[],
      });
    }

    // Decide async vs sync for the 3 custom CSV rules.
    if (rule.hasCustomValue && enabled && rawCustomValue) {
      const isLarge =
        rawCustomValue.length > ASYNC_CSV_CHAR_THRESHOLD ||
        countCsvEntries(rawCustomValue) > ASYNC_CSV_ENTRY_THRESHOLD;

      if (isLarge) {
        // Persist `enabled=true` immediately so the toggle state survives the
        // round trip; the worker will overwrite `customValue` once it finishes.
        await db.shopRule.upsert({
          where: {
            shopId_ruleKey: { shopId: shopDomain, ruleKey: rule.key },
          },
          create: {
            shopId: shopDomain,
            ruleKey: rule.key,
            enabled: true,
            customValue: null,
          },
          update: { enabled: true },
        });

        const jobId = await enqueueJob(JobType.PROCESS_BULK_IMPORT, {
          shopId: shopDomain,
          ruleKey: rule.key,
          csvText: rawCustomValue,
        });
        asyncJobs.push({
          ruleKey: rule.key,
          jobId,
          ruleLabel: rule.customValueLabel ?? rule.label,
        });
        continue;
      }
    }

    let customValue = rawCustomValue;
    if (customValue) {
      customValue = customValue
        .split(",")
        .map((v) => v.trim())
        .filter((v) => v.length > 0)
        .join(", ");
    }

    const finalEnabled =
      rule.hasCustomValue && enabled && (!customValue || customValue === "")
        ? false
        : enabled;

    syncUpdates.push({
      ruleKey: rule.key,
      enabled: finalEnabled,
      customValue,
    });
  }

  for (const update of syncUpdates) {
    await db.shopRule.upsert({
      where: {
        shopId_ruleKey: {
          shopId: shopDomain,
          ruleKey: update.ruleKey,
        },
      },
      create: {
        shopId: shopDomain,
        ruleKey: update.ruleKey,
        enabled: update.enabled,
        customValue: update.customValue ?? null,
      },
      update: {
        enabled: update.enabled,
        customValue: update.customValue ?? null,
      },
    });
  }

  const result = await syncRulesToMetafield(admin, shopDomain);

  if (!result.success) {
    return json({
      success: false,
      error: result.error,
      lastSyncedAt: null,
      asyncJobs,
    });
  }

  const shop = await db.shop.findUnique({ where: { id: shopDomain } });

  return json({
    success: true,
    error: null,
    lastSyncedAt: shop?.lastSyncedAt?.toISOString() ?? null,
    asyncJobs,
  });
};

export default function Dashboard() {
  const { shopDomain, rulesState, lastSyncedAt: loaderSyncedAt, shopTier } =
    useLoaderData<typeof loader>();
  const actionData = useActionData<typeof action>();
  const submit = useSubmit();
  const navigation = useNavigation();

  const isSubmitting = navigation.state === "submitting";

  const [selectedTab, setSelectedTab] = useState(0);
  const [localRules, setLocalRules] =
    useState<Record<string, { enabled: boolean; customValue: string }>>(
      rulesState,
    );
  const [isDirty, setIsDirty] = useState(false);
  const [showBanner, setShowBanner] = useState(false);
  const [trackedJobs, setTrackedJobs] = useState<
    { jobId: string; ruleLabel: string; status: string; lastError?: string | null }[]
  >([]);

  const displaySyncedAt = actionData?.lastSyncedAt ?? loaderSyncedAt;

  useEffect(() => {
    if (actionData) {
      setShowBanner(true);
      if (actionData.success) {
        setIsDirty(false);
        // Sync local state: disable toggles for custom rules with empty values
        setLocalRules((prev) => {
          const next = { ...prev };
          for (const rule of RULES) {
            if (rule.hasCustomValue && next[rule.key].enabled && !next[rule.key].customValue.trim()) {
              next[rule.key] = { ...next[rule.key], enabled: false };
            }
          }
          return next;
        });
      }
      const newJobs = actionData.asyncJobs ?? [];
      if (newJobs.length > 0) {
        setTrackedJobs((prev) => [
          ...prev.filter((j) => !newJobs.some((nj) => nj.jobId === j.jobId)),
          ...newJobs.map((j) => ({
            jobId: j.jobId,
            ruleLabel: j.ruleLabel,
            status: "pending",
          })),
        ]);
      }
    }
  }, [actionData]);

  // Poll in-progress jobs every 2.5s. Read the latest list via a ref so we
  // don't recreate the interval every tick. The effect only restarts when the
  // set of in-flight job ids changes, not on every status mutation.
  const trackedJobsRef = useRef(trackedJobs);
  useEffect(() => {
    trackedJobsRef.current = trackedJobs;
  }, [trackedJobs]);

  const inFlightKey = trackedJobs
    .filter((j) => j.status === "pending" || j.status === "active")
    .map((j) => j.jobId)
    .sort()
    .join(",");

  useEffect(() => {
    if (inFlightKey === "") return;

    const interval = setInterval(async () => {
      const current = trackedJobsRef.current;
      const updated = await Promise.all(
        current.map(async (job) => {
          if (job.status === "completed" || job.status === "failed") return job;
          try {
            const res = await fetch(`/api/jobs/${job.jobId}`);
            if (!res.ok) return job;
            const data = (await res.json()) as {
              status: string;
              lastError: string | null;
            };
            return { ...job, status: data.status, lastError: data.lastError };
          } catch {
            return job;
          }
        }),
      );
      setTrackedJobs(updated);
    }, 2500);

    return () => clearInterval(interval);
  }, [inFlightKey]);

  const handleTabChange = useCallback(
    (index: number) => setSelectedTab(index),
    [],
  );

  const handleToggle = useCallback((ruleKey: string) => {
    setLocalRules((prev) => ({
      ...prev,
      [ruleKey]: { ...prev[ruleKey], enabled: !prev[ruleKey].enabled },
    }));
    setIsDirty(true);
  }, []);

  const handleCustomValueChange = useCallback(
    (ruleKey: string, value: string) => {
      setLocalRules((prev) => ({
        ...prev,
        [ruleKey]: { ...prev[ruleKey], customValue: value },
      }));
      setIsDirty(true);
    },
    [],
  );

  const handleSave = useCallback(() => {
    const formData = new FormData();
    for (const rule of RULES) {
      formData.set(
        `rule_${rule.key}`,
        localRules[rule.key].enabled ? "true" : "false",
      );
      if (rule.hasCustomValue) {
        formData.set(`value_${rule.key}`, localRules[rule.key].customValue);
      }
    }
    submit(formData, { method: "post" });
  }, [localRules, submit]);

  const tabs = TAB_GROUPS.map((group) => ({
    id: group.key,
    content: group.label,
  }));

  const currentGroup = TAB_GROUPS[selectedTab];
  const currentRules = getRulesByTabGroup(currentGroup.key);

  return (
    <Page
      title=""
      titleMetadata={
        <img src="/cleanserp-logo.svg" alt="CleanSerp" style={{ height: "32px" }} />
      }
    >
      <Layout>
        <Layout.Section>
          {showBanner && actionData?.success === true && (
            <Banner tone="success" onDismiss={() => setShowBanner(false)}>
              <p>
                Configuration sauvegardée et synchronisée. Les changements peuvent prendre jusqu'à 5 minutes pour apparaître sur votre boutique.
              </p>
            </Banner>
          )}
          {showBanner && actionData?.success === false && (
            <Banner tone="critical" onDismiss={() => setShowBanner(false)}>
              <p>Erreur : {actionData.error}</p>
            </Banner>
          )}

          {trackedJobs.length > 0 && (
            <Box paddingBlockEnd="400">
              <BlockStack gap="200">
                {trackedJobs.map((job) => {
                  const inFlight =
                    job.status === "pending" || job.status === "active";
                  const tone = inFlight
                    ? "info"
                    : job.status === "completed"
                      ? "success"
                      : "critical";
                  const message = inFlight
                    ? `Import en cours pour "${job.ruleLabel}". Vous serez notifié une fois terminé.`
                    : job.status === "completed"
                      ? `Import terminé pour "${job.ruleLabel}". Rechargez pour voir la valeur consolidée.`
                      : `Import échoué pour "${job.ruleLabel}". ${job.lastError ?? ""}`;
                  return (
                    <Banner key={job.jobId} tone={tone}>
                      <p>{message}</p>
                    </Banner>
                  );
                })}
              </BlockStack>
            </Box>
          )}

          <Box paddingBlockEnd="400">
            <Banner tone="info">
              <p>
                L'extension <strong>CleanSerp SEO</strong> doit être activée et rester active dans votre thème.
                Allez dans{" "}
                <strong>
                  Boutique en ligne &gt; Thèmes &gt; Personnaliser &gt; App embeds
                </strong>
                {" "}et vérifiez que <strong>CleanSerp SEO</strong> est bien activé.
              </p>
            </Banner>
          </Box>

          <InlineStack align="space-between" blockAlign="center">
            {displaySyncedAt ? (
              <Text as="p" variant="bodySm" tone="subdued">
                Dernière synchronisation :{" "}
                {new Date(displaySyncedAt).toLocaleString("fr-FR")}
              </Text>
            ) : (
              <span />
            )}
            <Button
              variant="primary"
              onClick={handleSave}
              disabled={!isDirty || isSubmitting}
              loading={isSubmitting}
            >
              Sauvegarder
            </Button>
          </InlineStack>
        </Layout.Section>

        <Layout.Section>
          <Tabs tabs={tabs} selected={selectedTab} onSelect={handleTabChange}>
            <Box paddingBlockStart="400">
              <BlockStack gap="400">
                {currentRules.map((rule) => {
                  const state = localRules[rule.key];
                  return (
                    <RuleCard
                      key={rule.key}
                      rule={rule}
                      enabled={state.enabled}
                      customValue={state.customValue}
                      shopTier={shopTier}
                      shopDomain={shopDomain}
                      onToggle={() => handleToggle(rule.key)}
                      onCustomValueChange={(value: string) =>
                        handleCustomValueChange(rule.key, value)
                      }
                    />
                  );
                })}
              </BlockStack>
            </Box>
          </Tabs>
        </Layout.Section>

      </Layout>
    </Page>
  );
}

function RenderDescription({ text }: { text: string }) {
  const parts = text.split(/(`[^`]+`)/g);
  return (
    <Text as="p" variant="bodySm" tone="subdued">
      {parts.map((part, i) =>
        part.startsWith("`") && part.endsWith("`") ? (
          <span
            key={i}
            style={{
              backgroundColor: "#f3e8ff",
              color: "#7c3aed",
              padding: "1px 6px",
              borderRadius: "4px",
              fontFamily: "monospace",
              fontSize: "0.8125rem",
            }}
          >
            {part.slice(1, -1)}
          </span>
        ) : (
          <span key={i}>{part}</span>
        ),
      )}
    </Text>
  );
}

function SerpPreviewCard({
  preview,
  enabled,
}: {
  preview: SerpPreview;
  enabled: boolean;
}) {
  return (
    <div
      style={{
        border: "1px solid #e5e7eb",
        borderRadius: "8px",
        padding: "16px",
        backgroundColor: "#fff",
        position: "relative",
      }}
    >
      {enabled && (
        <span
          style={{
            position: "absolute",
            top: "12px",
            right: "12px",
            backgroundColor: "#dc2626",
            color: "white",
            fontSize: "0.6875rem",
            fontWeight: 600,
            padding: "2px 8px",
            borderRadius: "4px",
            textTransform: "uppercase",
            letterSpacing: "0.02em",
          }}
        >
          Désindexé
        </span>
      )}
      <div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "8px" }}>
        <div
          style={{
            width: "28px",
            height: "28px",
            borderRadius: "50%",
            backgroundColor: preview.faviconColor,
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            color: "white",
            fontSize: "0.8125rem",
            fontWeight: 600,
            flexShrink: 0,
          }}
        >
          {preview.faviconLetter}
        </div>
        <div style={{ minWidth: 0 }}>
          <div style={{ fontSize: "0.8125rem", fontWeight: 500, color: "#1a1a1a" }}>
            {preview.siteTitle}
          </div>
          <div
            style={{
              fontSize: "0.75rem",
              color: "#6b7280",
              whiteSpace: "nowrap",
              overflow: "hidden",
              textOverflow: "ellipsis",
            }}
          >
            {preview.siteUrl}
          </div>
        </div>
      </div>
      <div
        style={{
          fontSize: "0.9375rem",
          color: "#1a0dab",
          fontWeight: 400,
          marginBottom: "4px",
          textDecoration: "underline",
          textDecorationColor: "#1a0dab",
        }}
      >
        {preview.pageTitle}
      </div>
      <div style={{ fontSize: "0.8125rem", color: "#4d5156", lineHeight: 1.4 }}>
        {preview.pageDescription}
      </div>
    </div>
  );
}

const TIER_ORDER: Record<RuleTier, number> = { free: 0, standard: 1, pro: 2 };

function RuleCard({
  rule,
  enabled,
  customValue,
  shopTier,
  shopDomain,
  onToggle,
  onCustomValueChange,
}: {
  rule: RuleDefinition;
  enabled: boolean;
  customValue: string;
  shopTier: RuleTier;
  shopDomain: string;
  onToggle: () => void;
  onCustomValueChange: (value: string) => void;
}) {
  const isLocked = TIER_ORDER[rule.tier] > TIER_ORDER[shopTier];
  const isOverLimit = customValue.length > MAX_CUSTOM_VALUE_LENGTH;

  const robotsBadgeTone = !enabled
    ? undefined
    : rule.robotsValue.includes("nofollow")
      ? "critical"
      : "warning";

  return (
    <Card>
      <InlineGrid columns={rule.serpPreview ? ["oneHalf", "oneHalf"] : ["oneThird"]} gap="400">
        <BlockStack gap="200">
          <InlineStack gap="200" blockAlign="center" wrap>
            <Text as="h3" variant="headingSm">
              {rule.label}
            </Text>
            {isLocked ? (
              <Badge tone="attention">{rule.tier === "pro" ? "PRO" : "STANDARD"}</Badge>
            ) : (
              <>
                <Badge tone={enabled ? "success" : undefined}>
                  {enabled ? "ON" : "OFF"}
                </Badge>
                {enabled && (
                  <Badge tone={robotsBadgeTone}>
                    {rule.robotsValue === "noindex" ? "noindex,follow" : rule.robotsValue}
                  </Badge>
                )}
                {enabled && rule.hasCanonical && (
                  <Badge tone="info">+ canonical</Badge>
                )}
              </>
            )}
            <label
              style={{
                position: "relative",
                display: "inline-block",
                width: "40px",
                height: "22px",
                cursor: isLocked ? "not-allowed" : "pointer",
                opacity: isLocked ? 0.4 : 1,
                flexShrink: 0,
              }}
            >
              <input
                type="checkbox"
                checked={enabled}
                onChange={isLocked ? undefined : onToggle}
                disabled={isLocked}
                style={{ opacity: 0, width: 0, height: 0 }}
              />
              <span
                style={{
                  position: "absolute",
                  top: 0,
                  left: 0,
                  right: 0,
                  bottom: 0,
                  backgroundColor: enabled ? "#2c6ecb" : "#8c9196",
                  borderRadius: "11px",
                  transition: "background-color 0.15s ease",
                }}
              />
              <span
                style={{
                  position: "absolute",
                  top: "2px",
                  left: enabled ? "20px" : "2px",
                  width: "18px",
                  height: "18px",
                  backgroundColor: "white",
                  borderRadius: "50%",
                  transition: "left 0.15s ease",
                  boxShadow: "0 1px 2px rgba(0,0,0,0.15)",
                }}
              />
            </label>
          </InlineStack>
          <RenderDescription text={rule.description} />

          {isLocked && (
            <InlineStack gap="200" blockAlign="center">
              <Text as="p" variant="bodySm" tone="caution">
                <a href="/app/billing" style={{ color: "#b45309", textDecoration: "underline" }}>
                  Passez au plan {rule.tier === "pro" ? "Pro" : "Standard"}
                </a>{" "}
                pour activer cette règle.
              </Text>
            </InlineStack>
          )}

          {rule.hasCustomValue && enabled && !isLocked && (
            <>
              <Divider />
              <TextField
                label={rule.customValueLabel ?? "Valeurs"}
                value={customValue}
                onChange={onCustomValueChange}
                placeholder={rule.customValuePlaceholder}
                multiline={2}
                maxLength={MAX_CUSTOM_VALUE_LENGTH}
                showCharacterCount
                autoComplete="off"
                helpText="Séparez les valeurs par des virgules"
                error={
                  isOverLimit
                    ? `Maximum ${MAX_CUSTOM_VALUE_LENGTH} caractères`
                    : undefined
                }
              />
            </>
          )}
        </BlockStack>

        {rule.serpPreview && (
          <BlockStack gap="200">
            <Text as="p" variant="bodySm" tone="subdued" fontWeight="semibold">
              Exemple d'apercu Google
            </Text>
            <SerpPreviewCard preview={rule.serpPreview} enabled={enabled} />
            {rule.googleCheckUrl && (
              <Button
                variant="plain"
                url={rule.googleCheckUrl.replace("{domain}", shopDomain)}
                target="_blank"
              >
                Verifier sur Google
              </Button>
            )}
          </BlockStack>
        )}
      </InlineGrid>
    </Card>
  );
}
