// PriceFetchControl — fetch-price button with consent modal, spinner,
// best-effort warning (stocks only), and attribution display.
//
// Issue #158 — wires into SnapshotLineRow for priced-kind categories.
//
// Behavior rules (from spec §1 + ADR 0011):
//   - Hidden when useIsPremium() === false OR categoryKind !== 'priced'
//   - First use requires explicit consent (persisted in user_preferences)
//   - For stock assetType: shows a "best-effort" badge + dismissable warning
//     (once per session, in-memory only — NOT persisted)
//   - Manual unit_price input stays active in all error paths (this component
//     is purely additive)

import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Loader2, X } from "lucide-react";
import { useIsPremium } from "../../hooks/useIsPremium";
import { prices } from "../../services/balance.service";
import type { PriceError } from "../../services/balance.service";
import {
  getPreference,
  setPreference,
} from "../../services/userPreferenceService";

// ---------------------------------------------------------------------------
// Module-level session dismiss state for best-effort warning (ADR 0011 §garde-fous)
// ---------------------------------------------------------------------------
let _bestEffortDismissedThisSession = false;

// Exported for tests — resets the in-memory dismiss flag.
export function __resetBestEffortDismissForTests(): void {
  _bestEffortDismissedThisSession = false;
}

// Consent preference key (per-profile via per-profile SQLite DB).
const CONSENT_KEY = "price_fetching_consent";

interface PriceFetchControlProps {
  symbol: string;
  date: string; // YYYY-MM-DD
  categoryKind: "simple" | "priced";
  assetType: "stock" | "crypto";
  onPriceFetched: (price: number, currency: string) => void;
}

/**
 * Check whether the user has already given consent for price fetching.
 * Returns true when a non-empty consent record exists in user_preferences.
 */
async function hasConsent(): Promise<boolean> {
  try {
    const raw = await getPreference(CONSENT_KEY);
    return !!raw;
  } catch {
    return false;
  }
}

/** Persist consent (consented_at + version shape). */
async function writeConsent(): Promise<void> {
  await setPreference(
    CONSENT_KEY,
    JSON.stringify({ consented_at: new Date().toISOString(), version: 1 })
  );
}

export default function PriceFetchControl({
  symbol,
  date,
  categoryKind,
  assetType,
  onPriceFetched,
}: PriceFetchControlProps) {
  const { t, i18n } = useTranslation();
  const isPremium = useIsPremium();

  // Local UI state
  const [showConsentModal, setShowConsentModal] = useState(false);
  const [isFetching, setIsFetching] = useState(false);
  const [error, setError] = useState<PriceError | null>(null);
  const [attribution, setAttribution] = useState<string | null>(null);
  // Whether the best-effort warning is currently shown (stock only).
  const [showBestEffortWarning, setShowBestEffortWarning] = useState(
    assetType === "stock" && !_bestEffortDismissedThisSession
  );

  // Keep the warning display in sync when the session-level flag is updated
  // from a sibling instance (e.g. multiple priced rows dismiss in sequence).
  useEffect(() => {
    if (assetType === "stock") {
      setShowBestEffortWarning(!_bestEffortDismissedThisSession);
    }
  }, [assetType]);

  // Hidden for non-premium users or non-priced categories.
  if (!isPremium || categoryKind !== "priced") {
    return null;
  }

  const dismissBestEffortWarning = () => {
    _bestEffortDismissedThisSession = true;
    setShowBestEffortWarning(false);
  };

  /** Actually trigger the price fetch (called after consent is confirmed). */
  const doFetch = async () => {
    setIsFetching(true);
    setError(null);
    setAttribution(null);

    const result = await prices.fetchPrice(symbol, date);

    setIsFetching(false);

    if (result.ok) {
      onPriceFetched(result.price, result.currency);
      // Show attribution with the fetched_at timestamp formatted to locale date.
      const fetchedAt = new Date(result.fetched_at);
      const formattedDate = fetchedAt.toLocaleDateString(
        i18n.language === "fr" ? "fr-CA" : "en-CA"
      );
      setAttribution(t("balance.priceFetching.attribution", { date: formattedDate }));
    } else {
      setError(result.error);
    }
  };

  /** Handle the main button click: check consent, then fetch or show modal. */
  const handleClick = async () => {
    if (isFetching) return;
    setError(null);
    setAttribution(null);

    const consented = await hasConsent();
    if (!consented) {
      setShowConsentModal(true);
    } else {
      await doFetch();
    }
  };

  /** User accepted in the consent modal. */
  const handleConsentAccept = async () => {
    setShowConsentModal(false);
    try {
      await writeConsent();
    } catch {
      // Non-blocking — proceed with fetch even if pref write failed.
    }
    await doFetch();
  };

  /** User declined in the consent modal. */
  const handleConsentDecline = () => {
    setShowConsentModal(false);
  };

  // Build the error i18n args.
  const errorMessage = error
    ? t(error.i18nKey, {
        seconds:
          "retry_after_s" in error ? Math.ceil(error.retry_after_s) : undefined,
        minutes:
          "retry_after_s" in error
            ? Math.ceil(error.retry_after_s / 60)
            : undefined,
        defaultValue: error.i18nKey,
      })
    : null;

  return (
    <div className="flex flex-col gap-1">
      {/* Stock best-effort warning — shown once per session, dismissable */}
      {assetType === "stock" && showBestEffortWarning && (
        <div className="flex items-start gap-1 text-[10px] text-[var(--muted-foreground)] bg-[var(--muted)]/60 rounded px-2 py-1">
          <span className="flex-1">{t("balance.priceFetching.bestEffortNotice")}</span>
          <button
            type="button"
            aria-label={t("common.close")}
            onClick={dismissBestEffortWarning}
            className="shrink-0 text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
          >
            <X size={10} />
          </button>
        </div>
      )}

      <div className="flex items-center gap-2">
        {/* Fetch button */}
        <button
          type="button"
          onClick={handleClick}
          disabled={isFetching}
          className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border border-[var(--border)] text-xs font-medium text-[var(--foreground)] bg-[var(--card)] hover:bg-[var(--muted)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
          aria-label={t("balance.priceFetching.button")}
        >
          {isFetching ? (
            <Loader2 size={12} className="animate-spin" />
          ) : null}
          {t("balance.priceFetching.button")}
          {/* Best-effort badge (stock only) */}
          {assetType === "stock" && (
            <span className="ml-0.5 text-[9px] uppercase tracking-wide px-1 py-0.5 rounded bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400">
              best-effort
            </span>
          )}
        </button>

        {/* Attribution line — shown after a successful fetch */}
        {attribution && !isFetching && (
          <span className="text-[10px] text-[var(--muted-foreground)]">
            {attribution}
          </span>
        )}
      </div>

      {/* Inline error message */}
      {errorMessage && !isFetching && (
        <p
          role="alert"
          className="text-xs text-[var(--negative)] mt-0.5"
          data-testid="price-fetch-error"
        >
          {errorMessage}
        </p>
      )}

      {/* Consent modal — rendered inline, portaled via fixed positioning */}
      {showConsentModal && (
        <ConsentModal
          onAccept={handleConsentAccept}
          onDecline={handleConsentDecline}
        />
      )}
    </div>
  );
}

// ---------------------------------------------------------------------------
// ConsentModal — minimal overlay, no external modal lib required
// ---------------------------------------------------------------------------

function ConsentModal({
  onAccept,
  onDecline,
}: {
  onAccept: () => void;
  onDecline: () => void;
}) {
  const { t } = useTranslation();
  return (
    <div
      className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
      role="dialog"
      aria-modal="true"
      aria-labelledby="price-consent-title"
    >
      <div className="bg-[var(--card)] rounded-xl border border-[var(--border)] shadow-xl max-w-md w-full p-6">
        <h2
          id="price-consent-title"
          className="text-base font-semibold mb-2"
        >
          {t("balance.priceFetching.consent.title")}
        </h2>
        <p className="text-sm text-[var(--muted-foreground)] mb-5">
          {t("balance.priceFetching.consent.body")}
        </p>
        <div className="flex justify-end gap-2">
          <button
            type="button"
            onClick={onDecline}
            className="px-4 py-2 rounded-lg border border-[var(--border)] text-sm hover:bg-[var(--muted)]"
          >
            {t("balance.priceFetching.consent.decline")}
          </button>
          <button
            type="button"
            onClick={onAccept}
            className="px-4 py-2 rounded-lg bg-[var(--primary)] text-white text-sm font-medium hover:opacity-90"
          >
            {t("balance.priceFetching.consent.accept")}
          </button>
        </div>
      </div>
    </div>
  );
}
