/**
 * AI Overview Click Recovery — segment container for OpportunityMap.
 *
 * Renders the summary stat row + detected-page cards (or empty state).
 * Fires `analytics.aio_recovery_viewed` on mount when items are present.
 *
 * W4-B additions:
 *   - "Generate all" button with cost-preview modal before commit
 *   - onGenerateDraft per-card callback → opens single-item cost preview modal
 *   - onRealized callback fires analytics.aio_recovery_realized once per
 *     item with a first positive roi_snapshot.clicks_delta
 *
 * Ownership: W1-D (base), W4-B (generate + realized). Consumed by OpportunityMap/Index.tsx.
 *
 * Prop contract (from implementation-waves doc § Shared Contracts):
 *   aio_recovery: { enabled: bool, run_id: int|null, summary: { pages, clicks_at_risk, est_recoverable }, items: AioRecoveryItem[] }
 */
import axios from 'axios';
import { Brain, Sparkles } from 'lucide-react';

import { useCallback, useEffect, useRef, useState } from 'react';

import type { Page } from '@inertiajs/core';
import { router } from '@inertiajs/react';

import SafeRefreshNudge from '@/Components/publish/SafeRefreshNudge';
import { Button } from '@/Components/ui/button';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from '@/Components/ui/dialog';
import { InlineLoader } from '@/Components/ui/inline-loader';
import { LoadingButton } from '@/Components/ui/loading-button';
import { trackEvent } from '@/lib/analytics';
import { AIO_RECOVERY_REALIZED, AIO_RECOVERY_VIEWED } from '@/lib/event-catalog';
import { formatCurrency, formatNumber } from '@/lib/format';
import { fireToast } from '@/lib/toast-config';
import { cn } from '@/lib/utils';

import { AioRecoveryCard } from './AioRecoveryCard';
import { AioRecoveryEmptyState } from './AioRecoveryEmptyState';
import { AioSummaryStats } from './AioSummaryStats';
import type { AioRecoveryProp } from './types';

// ─── Cost estimate response shape (from aio-recovery.batch-estimate) ──────────

interface AioCostEstimate {
  estimated_total_tokens: number;
  estimated_cost: number;
  recommendation_count: number;
  /**
   * BHUNT-API-02: The batch-estimate endpoint returns `linked_item_count` (the count of
   * actionable items with a recommendation_id set, before eligibility filter), NOT
   * `eligible_item_count` which was a dead field never returned by the backend.
   * See AioRecoveryController::estimateBatch (line ~155-157) and AioRecoveryBatchTest.
   */
  linked_item_count?: number;
  /**
   * FINDING SAFEREFRESH-AIO-PATH [P1]: SafeRefreshCapService cap for this user,
   * returned only by aio-recovery.batch-estimate (the "Generate all" path). The
   * single-item modal uses batch-ai.estimate, which does not return this field —
   * a single draft never approaches the cap.
   */
  safe_refresh_cap?: number;
}

// ─── Modal state discriminant ─────────────────────────────────────────────────

type ModalMode =
  | { kind: 'closed' }
  | { kind: 'single'; recommendationId: number }
  | { kind: 'all' };

// ─── Props ────────────────────────────────────────────────────────────────────

interface AioRecoverySegmentProps {
  aioRecovery: AioRecoveryProp;
  siteId: string;
  className?: string;
}

// ─── Component ────────────────────────────────────────────────────────────────

export function AioRecoverySegment({
  aioRecovery,
  siteId,
  className,
}: AioRecoverySegmentProps) {
  const { enabled, summary, items, run_id: runId } = aioRecovery;

  // BHUNT-FLOW-01: propagate the warming_up flag from the run summary so the empty state
  // can distinguish "not enough GSC history" from "detection ran and found nothing".
  const warmingUp = summary.warming_up ?? false;

  // Fire aio_recovery_viewed once per segment mount when enabled + items exist.
  // Also posts to the backend so audit_logs-backed analytics (FeatureAdoptionService,
  // FunnelMetricsService) can slice this event — trackEvent() alone reaches only GA4/PostHog.
  const firedRef = useRef(false);
  const serpConfirmedCount = items.filter((i) => i.serp_confirmed).length;
  useEffect(() => {
    if (!enabled || items.length === 0) return;
    if (firedRef.current) return;
    firedRef.current = true;
    trackEvent(AIO_RECOVERY_VIEWED, {
      site_id: String(siteId),
      items_shown: items.length,
      serp_confirmed_count: serpConfirmedCount,
    });
    axios.post(route('aio-recovery.record-event', siteId), {
      event_type: 'viewed',
      item_count: items.length,
    }).catch((err: unknown) => {
      console.warn('[AioRecovery] server mirror failed:', err);
      // Server mirror is best-effort — client-side tracking already fired.
    });
  }, [enabled, items.length, siteId, serpConfirmedCount]);

  // ── W4-B: cost preview modal ──────────────────────────────────────────────
  // All hooks must be declared before any conditional return (Rules of Hooks).
  const [modal, setModal] = useState<ModalMode>({ kind: 'closed' });
  const [estimate, setEstimate] = useState<AioCostEstimate | null>(null);
  const [estimateLoading, setEstimateLoading] = useState(false);
  const [estimateError, setEstimateError] = useState<string | null>(null);
  const [dispatching, setDispatching] = useState(false);


  // Fetch cost estimate whenever the modal opens.
  useEffect(() => {
    if (modal.kind === 'closed') return;

    setEstimate(null);
    setEstimateError(null);
    setEstimateLoading(true);

    let url: string;
    let body: Record<string, unknown>;

    if (modal.kind === 'single') {
      // Single item: use the generic batch-ai estimate endpoint (takes recommendation_ids[]).
      url = route('batch-ai.estimate', siteId);
      body = { recommendation_ids: [modal.recommendationId] };
    } else {
      // "Generate all": use the AIO-specific estimate endpoint (takes aio_recovery_run_id).
      url = route('aio-recovery.batch-estimate', siteId);
      body = runId !== null ? { aio_recovery_run_id: runId } : {};
    }

    axios
      .post<AioCostEstimate>(url, body)
      .then((res) => {
        setEstimate(res.data);
      })
      .catch((err: unknown) => {
        const msg =
          typeof err === 'object' &&
          err !== null &&
          'response' in err
            ? (err as { response?: { data?: { error?: string } } }).response?.data?.error
            : null;
        setEstimateError(msg ?? 'Could not calculate estimate. Check your AI settings.');
      })
      .finally(() => {
        setEstimateLoading(false);
      });
  // Intentionally excludes `runId` and `siteId` — those are stable for the lifetime
  // of the modal; including them would refetch on unrelated re-renders.
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [modal]);

  // ── W4-B: open single-item modal (called by AioRecoveryCard) ─────────────
  const handleGenerateDraft = useCallback((recommendationId: number) => {
    setModal({ kind: 'single', recommendationId });
  }, []);

  // ── W4-B: open "generate all" modal from segment header ──────────────────
  const handleGenerateAll = () => {
    setModal({ kind: 'all' });
  };

  // ── W4-B: dispatch batch — fire-and-forget (router.post never awaited) ────
  //
  // FE-BULK-DISPATCH-ONERROR (P1 fix + QA hardening):
  // The backend returns redirect()->back()->with('error') for all business failures
  // (AI key invalid, quota exceeded, etc.). Inertia follows the redirect and fires
  // onSuccess — so onError alone is NOT enough to catch business failures.
  //
  // Dual-layer guard:
  //  1. onError fires on 422 (Form Request validation) — keeps modal open, fires toast.
  //  2. onSuccess reads page.props.flash?.error from the redirect target. If flash.error
  //     is set, the server signalled a business failure via back()->with('error', ...) —
  //     keep the modal open. The server's flash error is already shown by useFlashToasts
  //     (global hook); onSuccess only fires our own toast when flash.error is absent
  //     (i.e. a true 2xx clean success redirect), otherwise stays silent to avoid duplicating.
  //     On a clean success the modal closes.
  const handleDispatch = () => {
    if (dispatching) return;
    setDispatching(true);

    const handleSuccess = (page: Page | undefined) => {
      const flashError = page && (page.props as { flash?: { error?: string } }).flash?.error;
      if (flashError) {
        // Server-side business failure (AI key / quota / etc.) arrived via redirect.
        // useFlashToasts already shows the server's error message — do NOT add a second toast.
        // Keep the modal open so the user can retry or fix the issue.
        return;
      }
      setModal({ kind: 'closed' });
    };

    const handleError = () => {
      // 422 / network error path: Inertia fires onError (not onSuccess) here.
      // useFlashToasts doesn't fire for 422; we need our own toast.
      fireToast('error', 'Could not start the rewrite batch — check your AI key and usage limit, then try again.');
    };

    if (modal.kind === 'single') {
      router.post(
        route('batch-ai.store', siteId),
        { recommendation_ids: [modal.recommendationId] },
        {
          onSuccess: handleSuccess,
          onError: handleError,
          onFinish: () => { setDispatching(false); },
        },
      );
    } else if (modal.kind === 'all' && runId !== null) {
      router.post(
        route('aio-recovery.batch-dispatch', siteId),
        { aio_recovery_run_id: runId },
        {
          onSuccess: handleSuccess,
          onError: handleError,
          onFinish: () => { setDispatching(false); },
        },
      );
    } else {
      // Defensive: if no branch fires router.post (e.g. runId became null after modal opened),
      // clear the dispatching spinner so the button is never permanently stuck.
      setDispatching(false);
    }
  };

  // ── W4-B: realized event — fired once per item on first positive delta ────
  // Also posts to the backend so audit_logs-backed analytics can slice this event.
  // The set persists across re-renders without triggering a state update.
  const realizedFiredSet = useRef<Set<string>>(new Set());

  const handleRealized = useCallback(
    (itemId: string, clicksDelta: number) => {
      if (realizedFiredSet.current.has(itemId)) return;
      realizedFiredSet.current.add(itemId);

      const item = items.find((i) => i.id === itemId);
      trackEvent(AIO_RECOVERY_REALIZED, {
        site_id: String(siteId),
        realized_clicks_delta: clicksDelta,
        estimated_recoverable: item?.est_clicks_recoverable ?? 0,
        realized_vs_estimated: clicksDelta / Math.max(item?.est_clicks_recoverable ?? 1, 1),
      });
      axios.post(route('aio-recovery.record-event', siteId), {
        event_type: 'realized',
        item_id: itemId,
        realized_clicks_delta: clicksDelta,
      }).catch((err: unknown) => {
        console.warn('[AioRecovery] server mirror failed:', err);
        // Server mirror is best-effort — client-side tracking already fired.
      });
    },
    [items, siteId],
  );

  // Count of items eligible for "Generate all" (linked recommendation + not low-recoverability).
  // P2-3 §11.1-A: low-bucket items are unrecoverable (AIO fully satisfies the query) — they
  // show the "Monitor" indicator on their card, so batch generation must exclude them too.
  // Generating a draft for a low-recoverability item would be misleading and wasteful.
  const generatableCount = items.filter(
    (i) => i.recommendation_id !== null && i.recoverability !== 'low',
  ).length;

  // FINDING SAFEREFRESH-AIO-PATH [P1]: derive the over-cap state from the loaded
  // estimate so the modal can nudge + disable dispatch BEFORE the user hits the
  // server-side rejection in AioRecoveryController::dispatchBatch. safe_refresh_cap
  // is only present on the "Generate all" estimate response (aio-recovery.batch-estimate);
  // the single-item modal (batch-ai.estimate) never approaches the cap.
  const isOverSafeRefreshCap =
    estimate?.safe_refresh_cap !== undefined &&
    estimate.recommendation_count > estimate.safe_refresh_cap;

  // Modal title/description resolved from modal state.
  const modalTitle =
    modal.kind === 'single'
      ? 'Generate recovery draft'
      : `Generate ${generatableCount} recovery drafts`;

  const modalDescription =
    modal.kind === 'single'
      ? 'Review the estimated cost before generating a recovery draft for this page.'
      : `Review the estimated cost before generating recovery drafts for all ${generatableCount} eligible pages.`;

  // Conditional return AFTER all hooks (Rules of Hooks compliance).
  if (!enabled) return null;

  return (
    <section
      className={cn('space-y-4', className)}
      aria-labelledby="aio-recovery-heading"
      data-testid="aio-recovery-segment"
    >
      {/* Section header row — W5-H: flex-wrap so "Generate all" button wraps
          to a second line on narrow viewports instead of overflowing */}
      <div className="flex flex-wrap items-center gap-2">
        <Brain className="size-4 shrink-0 text-muted-foreground" aria-hidden="true" />
        <h2
          id="aio-recovery-heading"
          className="text-sm font-semibold text-foreground"
        >
          AI Overview Click Recovery
        </h2>
        {items.length > 0 && (
          <span
            className="inline-flex items-center justify-center rounded-full bg-warning-soft text-warning-strong border border-warning-border text-xs font-semibold px-1.5 py-0 tabular-nums min-w-[1.25rem]"
            aria-label={`${items.length} pages affected`}
          >
            {items.length}
          </span>
        )}

        {/* W4-B: "Generate all" — only when ≥1 item has a linked recommendation and run_id is known */}
        {generatableCount > 0 && runId !== null && (
          <Button
            size="sm"
            variant="outline"
            className="ml-auto min-h-[44px] text-xs"
            onClick={handleGenerateAll}
            aria-label={`Generate recovery drafts for all ${generatableCount} eligible pages`}
            data-testid="generate-all-btn"
          >
            <Sparkles className="size-3.5 mr-1.5" aria-hidden="true" />
            Generate all ({generatableCount})
          </Button>
        )}
      </div>

      {/* Summary stats row — shown when items exist */}
      {items.length > 0 && <AioSummaryStats summary={summary} />}

      {/* Cards or empty state */}
      {items.length > 0 ? (
        <div className="space-y-3" role="list" aria-label="Pages affected by AI Overviews">
          {items.map((item, index) => (
            <div key={item.id} role="listitem">
              <AioRecoveryCard
                item={item}
                position={index + 1}
                siteId={siteId}
                onGenerateDraft={handleGenerateDraft}
                onRealized={handleRealized}
              />
            </div>
          ))}
        </div>
      ) : (
        // BHUNT-FLOW-01: pass warming_up so the empty state shows "Still gathering data"
        // for new sites with <90 days of GSC history, not a false all-clear.
        <AioRecoveryEmptyState warmingUp={warmingUp} />
      )}

      {/* ── W4-B: Cost preview modal ─────────────────────────────────────────── */}
      <Dialog
        open={modal.kind !== 'closed'}
        onOpenChange={(open) => {
          if (!open && !dispatching) setModal({ kind: 'closed' });
        }}
      >
        <DialogContent>
          <DialogHeader>
            <DialogTitle>{modalTitle}</DialogTitle>
            <DialogDescription>{modalDescription}</DialogDescription>
          </DialogHeader>

          <div className="py-2">
            {estimateLoading && (
              <InlineLoader label="Calculating cost estimate…" />
            )}

            {estimateError && (
              <p className="text-sm text-destructive" role="alert">
                {estimateError}
              </p>
            )}

            {!estimateLoading && !estimateError && estimate && (
              <div
                className="rounded-md border border-border bg-muted/30 px-4 py-3 space-y-2 text-sm"
                aria-label="Cost estimate"
                data-testid="cost-estimate"
              >
                <div className="flex items-center justify-between">
                  <span className="text-muted-foreground">Drafts to generate</span>
                  <span className="font-semibold tabular-nums">
                    {formatNumber(estimate.recommendation_count)}
                  </span>
                </div>
                <div className="flex items-center justify-between">
                  <span className="text-muted-foreground">Estimated tokens</span>
                  <span className="font-semibold tabular-nums">
                    {formatNumber(estimate.estimated_total_tokens)}
                  </span>
                </div>
                <div className="flex items-center justify-between border-t border-border pt-2 mt-2">
                  <span className="text-foreground font-medium">Estimated cost</span>
                  <span className="font-bold tabular-nums">
                    {formatCurrency(estimate.estimated_cost)}
                  </span>
                </div>
              </div>
            )}

            {/* FINDING SAFEREFRESH-AIO-PATH [P1]: nudge before commit when the eligible
                count exceeds the safe-refresh cap — mirrors BulkPublishRequest's gate. */}
            {!estimateLoading && !estimateError && estimate?.safe_refresh_cap !== undefined && (
              <div className="mt-3">
                <SafeRefreshNudge
                  selectedCount={estimate.recommendation_count}
                  cap={estimate.safe_refresh_cap}
                />
              </div>
            )}
          </div>

          <DialogFooter className="gap-2">
            <Button
              variant="outline"
              onClick={() => setModal({ kind: 'closed' })}
              disabled={dispatching}
            >
              Cancel
            </Button>
            <LoadingButton
              loading={dispatching}
              disabled={estimateLoading || !!estimateError || dispatching || isOverSafeRefreshCap}
              onClick={handleDispatch}
              data-testid="confirm-generate-btn"
            >
              <Sparkles className="size-4 mr-1.5" aria-hidden="true" />
              {isOverSafeRefreshCap ? 'Over safe-refresh cap' : 'Generate'}
            </LoadingButton>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </section>
  );
}
