import React, { ReactNode, useState } from 'react';
import { LuChevronDown } from 'react-icons/lu';

import type { WinLossDetail } from '../../service/visibility/visibility.interface';

import ArticleModal from './ArticleModal';
import ArticleProgress from './ArticleProgress';
import GenerateLanguagesDialog from './GenerateLanguagesDialog';
import { EngineOutcomeChips, WonByChips } from './OutcomeChips';
import { PromptResponseEntryCard } from './PromptResponseModal';
import { useLossArticleGeneration } from './useLossArticleGeneration';
import {
  PromptProof,
  proofEntriesForWeek,
  usePromptProof,
} from './usePromptProof';

/** Props for {@link TopWinsLosses}. */
interface TopWinsLossesProps {
  topWins: string[];
  topLosses: string[];
  /**
   * Structured mirrors from the weekly report; empty on legacy reports, in
   * which case rows render as plain, non-interactive text.
   */
  topWinsDetail: WinLossDetail[];
  topLossesDetail: WinLossDetail[];
  brandId: string;
  clientId: string;
  token: string;
  language?: string | null;
  /** Own domain, highlighted among cited sources in the proof layer. */
  brandDomain?: string | null;
  /** The report's ISO week, so the proof layer matches the chips. */
  weekIso?: string | null;
  /**
   * Bubbled up when a loss row's generated article finishes. Lets the parent
   * dashboard refetch the Articles list / tab counts so the new article
   * appears in the Recommended section without a manual page refresh.
   */
  onArticleGenerated?: () => void;
}

/** Upper bound on rows rendered per card. */
const MAX_ROWS = 5;

/**
 * The week's structured detail for one win/loss text, when the report carries
 * it. Matched case-insensitively: the plain list and the detail list are two
 * renderings of the same localized string.
 *
 * @param {WinLossDetail[]} details - The report's detail list.
 * @param {string} text - The row's prompt text.
 * @returns {WinLossDetail | null} The matching detail, or ``null``.
 */
const detailForText = (
  details: WinLossDetail[],
  text: string
): WinLossDetail | null => {
  const key = text.trim().toLowerCase();

  return (
    details.find(detail => detail.text.trim().toLowerCase() === key) ?? null
  );
};

/** Props for {@link ProofBlock}. */
interface ProofBlockProps {
  /** Fetch state for this row's prompt. */
  proof: PromptProof | null;
  /** The merchant's own domain, for source highlighting. */
  brandDomain: string | null;
  /** The report's week, so the proof matches the row's verdict chips. */
  weekIso: string | null;
}

/**
 * The expanded proof layer under a row: what each engine actually said,
 * lazily fetched the first time the row opens.
 *
 * @param {ProofBlockProps} props - Fetch state, own domain and report week.
 * @returns {JSX.Element} Proof cards, a skeleton, or the failure line.
 */
const ProofBlock = ({
  proof,
  brandDomain,
  weekIso,
}: ProofBlockProps): JSX.Element => {
  if (!proof || proof.status === 'loading') {
    return (
      <div className="mt-3 pl-[30px]">
        <div className="h-16 animate-pulse rounded-xl bg-gray-100" />
      </div>
    );
  }
  if (proof.status === 'failed') {
    return (
      <p className="mt-3 pl-[30px] text-xs text-gray-500">
        Could not load the AI responses for this prompt. Close and reopen the
        row to retry.
      </p>
    );
  }
  const entries = proofEntriesForWeek(proof.entries, weekIso);
  if (entries.length === 0) {
    return (
      <p className="mt-3 pl-[30px] text-xs text-gray-500">
        No recorded responses for this prompt yet.
      </p>
    );
  }

  return (
    <div className="mt-3 flex flex-col gap-3 pl-[30px]">
      {entries.map(entry => (
        <PromptResponseEntryCard
          key={entry.engine}
          entry={entry}
          brandDomain={brandDomain}
        />
      ))}
    </div>
  );
};

/** Props for {@link WinLossRow}. */
interface WinLossRowProps {
  text: string;
  kind: 'win' | 'loss';
  /** This week's structured detail, or ``null`` on legacy reports. */
  detail: WinLossDetail | null;
  expanded: boolean;
  onToggle: () => void;
  proof: PromptProof | null;
  brandDomain: string | null;
  /** The report's week, forwarded to the proof layer. */
  weekIso: string | null;
  /** Right-edge controls (the loss rows' Generate button). */
  actions?: ReactNode;
  /** Extra blocks inside the row (generation error / progress). */
  children?: ReactNode;
}

/**
 * One row of either card: status marker, the prompt text (a disclosure button
 * when the proof layer is reachable), the week's verdict chips, and the
 * expanded proof. Module-level on purpose: defined inside the parent it would
 * remount - and drop focus plus expansion state - on every parent render.
 *
 * @param {WinLossRowProps} props - Row content and interaction state.
 * @returns {JSX.Element} The row, as exactly one list item.
 */
const WinLossRow = ({
  text,
  kind,
  detail,
  expanded,
  onToggle,
  proof,
  brandDomain,
  weekIso,
  actions,
  children,
}: WinLossRowProps): JSX.Element => {
  const expandable = Boolean(detail?.prompt_id);

  return (
    <li className="py-2.5">
      <div className="flex flex-wrap items-start justify-between gap-2">
        <div className="flex min-w-0 flex-1 items-start gap-2.5">
          <span
            aria-hidden="true"
            className={`mt-0.5 inline-flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-full text-xs font-bold ${
              kind === 'win'
                ? 'bg-green-100 text-green-800'
                : 'bg-orange-100 text-orange-800'
            }`}
          >
            {kind === 'win' ? '✓' : '!'}
          </span>
          <div className="flex min-w-0 flex-1 flex-col gap-1.5">
            {/* The disclosure button holds the text and the chevron ONLY;
                the chips sit outside it so they are never swallowed by the
                row's click target. */}
            {expandable ? (
              <button
                type="button"
                aria-expanded={expanded}
                onClick={onToggle}
                title="See what the AI said"
                className="inline-flex cursor-pointer items-start gap-1.5 text-left text-sm font-medium text-gray-900"
              >
                <span className="min-w-0">{text}</span>
                <LuChevronDown
                  aria-hidden
                  className={`mt-0.5 h-3.5 w-3.5 shrink-0 text-gray-400 transition-transform ${
                    expanded ? 'rotate-180' : ''
                  }`}
                />
              </button>
            ) : (
              <span className="block text-sm font-medium text-gray-900">
                {text}
              </span>
            )}
            {detail && detail.engines.length > 0 && (
              <EngineOutcomeChips outcomes={detail.engines} />
            )}
            {kind === 'loss' && detail && detail.won_by.length > 0 && (
              <WonByChips
                rivals={detail.won_by}
                // "Won by" only fits a lost prompt; when the brand is in the
                // answer the rivals merely share it.
                label={
                  detail.engines.some(entry => entry.mentioned)
                    ? 'Also in the answer'
                    : 'Won by'
                }
              />
            )}
          </div>
        </div>
        {actions}
      </div>
      {expanded && expandable && (
        <ProofBlock proof={proof} brandDomain={brandDomain} weekIso={weekIso} />
      )}
      {children}
    </li>
  );
};

/**
 * Two side-by-side cards: "Where you appeared this week" (wins) and "Where you
 * missed out" (losses). The weekly report's structured detail drives
 * per-engine verdict chips and, on losses, the rivals who won the prompt;
 * expanding a row lazily fetches the engine's actual answer and cited sources.
 * Loss rows keep the Generate-content dispatch.
 *
 * @param {TopWinsLossesProps} props - The week's rows, detail and auth.
 * @returns {JSX.Element | null} The pair of cards, or ``null`` when empty.
 */
const TopWinsLosses = ({
  topWins,
  topLosses,
  topWinsDetail,
  topLossesDetail,
  brandId,
  clientId,
  token,
  language,
  brandDomain = null,
  weekIso = null,
  onArticleGenerated,
}: TopWinsLossesProps): JSX.Element | null => {
  const generation = useLossArticleGeneration({
    clientId,
    token,
    brandId,
    language,
    onArticleGenerated,
  });
  const proof = usePromptProof(clientId, token, brandId);
  // Keyed by kind + text (not index), so a week switch can never leave the
  // expansion pointing at a different prompt's proof.
  const [expandedKey, setExpandedKey] = useState<string | null>(null);

  if (topWins.length === 0 && topLosses.length === 0) return null;

  const visibleWins = topWins.slice(0, MAX_ROWS);
  const visibleLosses = topLosses
    .slice(0, MAX_ROWS)
    .filter(loss => generation.runFor(loss)?.phase !== 'covered');

  const toggleRow = (key: string, detail: WinLossDetail | null): void => {
    const next = expandedKey === key ? null : key;
    setExpandedKey(next);
    if (next && detail?.prompt_id) proof.loadProof(detail.prompt_id);
  };

  return (
    <>
      {/* Default grid stretch on purpose: the two cards stay equal height,
          per Daniel - mismatched bottoms looked worse than the empty space. */}
      <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
        <div className="rounded-xl border border-gray-200 bg-white p-4">
          <div className="flex flex-col gap-3">
            <div>
              <h3 className="text-base font-semibold text-gray-900">
                Where you appeared this week
              </h3>
              <p className="mt-0.5 text-xs text-gray-500">
                Customer questions where AI assistants surfaced your brand, with
                each engine&apos;s verdict. Open a row to read exactly what the
                AI said and which sources it cited.
              </p>
            </div>
            {visibleWins.length === 0 ? (
              <p className="text-sm text-gray-500">
                No wins recorded for this week.
              </p>
            ) : (
              <ul className="m-0 list-none divide-y divide-gray-200 p-0">
                {visibleWins.map(win => {
                  const detail = detailForText(topWinsDetail, win);
                  const key = `win:${win}`;

                  return (
                    <WinLossRow
                      key={key}
                      text={win}
                      kind="win"
                      detail={detail}
                      expanded={expandedKey === key}
                      onToggle={() => toggleRow(key, detail)}
                      proof={
                        detail?.prompt_id
                          ? proof.proofFor(detail.prompt_id)
                          : null
                      }
                      brandDomain={brandDomain}
                      weekIso={weekIso}
                    />
                  );
                })}
              </ul>
            )}
          </div>
        </div>

        <div className="rounded-xl border border-gray-200 bg-white p-4">
          <div className="flex flex-col gap-3">
            <div>
              <h3 className="text-base font-semibold text-gray-900">
                Where you missed out
              </h3>
              <p className="mt-0.5 text-xs text-gray-500">
                Questions your customers ask where AI skipped you - and who it
                recommended instead. Generate content to win the prompt back.
              </p>
            </div>
            {visibleLosses.length === 0 ? (
              <p className="text-sm text-gray-500">
                No missed opportunities flagged this week.
              </p>
            ) : (
              <ul className="m-0 list-none divide-y divide-gray-200 p-0">
                {visibleLosses.map(loss => {
                  const detail = detailForText(topLossesDetail, loss);
                  const key = `loss:${loss}`;
                  const run = generation.runFor(loss);

                  return (
                    <WinLossRow
                      key={key}
                      text={loss}
                      kind="loss"
                      detail={detail}
                      expanded={expandedKey === key}
                      onToggle={() => toggleRow(key, detail)}
                      proof={
                        detail?.prompt_id
                          ? proof.proofFor(detail.prompt_id)
                          : null
                      }
                      brandDomain={brandDomain}
                      weekIso={weekIso}
                      actions={
                        <button
                          type="button"
                          onClick={() => generation.openLanguageDialog(loss)}
                          disabled={run?.phase === 'generating'}
                          className="inline-flex shrink-0 items-center rounded-md border border-gray-300 bg-white px-2.5 py-1 text-xs font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
                        >
                          {run?.phase === 'generating'
                            ? 'Generating…'
                            : 'Generate content'}
                        </button>
                      }
                    >
                      {run?.phase === 'failed' && (
                        <p className="mt-2 text-xs text-red-700">
                          {run.message}
                        </p>
                      )}
                      {run?.phase === 'generating' && (
                        <div className="mt-2">
                          <ArticleProgress
                            articleId={run.articleId}
                            clientId={clientId}
                            token={token}
                            onReady={article =>
                              generation.markReady(loss, article.summary)
                            }
                            onFailed={message =>
                              generation.markFailed(loss, message)
                            }
                          />
                        </div>
                      )}
                    </WinLossRow>
                  );
                })}
              </ul>
            )}
          </div>
        </div>
      </div>

      <ArticleModal
        open={generation.openArticleId !== null}
        articleId={generation.openArticleId}
        onClose={generation.closeArticleModal}
        clientId={clientId}
        token={token}
      />

      <GenerateLanguagesDialog
        open={generation.dialogTopic !== null}
        onOpenChange={next => {
          if (!next) generation.closeLanguageDialog();
        }}
        primaryLanguage={language || 'en'}
        busy={generation.dialogBusy}
        onConfirm={languages => {
          void generation.confirmLanguages(languages);
        }}
      />
    </>
  );
};

export default TopWinsLosses;
