import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useState,
  type ReactNode,
} from "react";
import { toast } from "sonner";
import {
  addQueryMessageApi,
  answerQueryApi,
  approveBankMatchApi,
  approveInvoiceApi,
  approveQueryApi,
  bulkApproveApi,
  bulkNeedsReviewApi,
  cancelQueryApi,
  checkApiHealth,
  createClientApi,
  createQueryApi,
  fetchLedgerState,
  ignoreBankTransactionApi,
  isApiEnabled,
  rejectBankMatchApi,
  rejectInvoiceApi,
  resolveQueryApi,
  setClientRuleApi,
  setDuplicateStatusApi,
  setInvoiceStatusApi,
  setTxStatusApi,
  syncBankTransactionsApi,
  syncInvoiceApi,
  updateInvoiceApi,
  updateSettingsApi,
  updateSuggestionApi,
  uploadDocumentFileApi,
  DuplicateUploadError,
  type FirmActor,
} from "./api";
import { EMPTY_LEDGER_STATE } from "./empty-ledger";
import type {
  ActivityEvent,
  BankTx,
  Client,
  ClientQuery,
  Invoice,
  InvoiceStatus,
  Settings,
  Suggestion,
} from "./ledger-types";
import type { AuthPlatform } from "./auth";
import { useAuthSession } from "@/hooks/useAuthSession";
import { WorkspaceSplash } from "@/components/brand/WorkspaceSplash";

interface Ctx {
  ready: boolean;
  loadError: boolean;
  clients: Client[];
  invoices: Invoice[];
  transactions: BankTx[];
  queries: ClientQuery[];
  activity: ActivityEvent[];
  settings: Settings;
  clientName: (id: string) => string;
  getInvoice: (id: string) => Invoice | undefined;
  refreshState: () => Promise<void>;
  createClient: (
    payload: {
      name: string;
      companyName?: string;
      email: string;
      password: string;
      kvk?: string;
    },
    actor: FirmActor,
  ) => Promise<Client>;
  setDuplicateStatus: (id: string, status: string) => void;
  uploadDocumentFile: (
    file: File,
    options: {
      platform: AuthPlatform;
      email: string;
      clientId?: string;
      actor: string;
      forceDuplicate?: boolean;
      onProgress?: (percent: number) => void;
    },
  ) => Promise<Invoice>;
  updateInvoiceFields: (id: string, patch: Partial<Invoice>) => void;
  updateSuggestion: (id: string, patch: Partial<Suggestion>) => void;
  setInvoiceStatus: (id: string, status: InvoiceStatus) => void;
  approveInvoice: (id: string) => void;
  rejectInvoice: (id: string, reason: string) => void;
  syncInvoice: (id: string) => void;
  bulkApprove: (ids: string[]) => void;
  bulkNeedsReview: (ids: string[]) => void;
  createQuery: (
    clientId: string,
    question: string,
    opts?: { invoiceId?: string; title?: string; priority?: string },
  ) => void;
  answerQuery: (id: string, answer: string, proof?: string) => Promise<void>;
  replyToQuery: (id: string, message: string, attachmentRef?: string) => Promise<void>;
  resolveQuery: (id: string) => Promise<void>;
  cancelQuery: (id: string, reason?: string) => Promise<void>;
  approveQuery: (id: string) => void;
  setTxStatus: (id: string, status: BankTx["status"]) => void;
  approveBankMatch: (txId: string, invoiceId: string, amount?: number) => void;
  rejectBankMatch: (txId: string) => void;
  ignoreBankTransaction: (txId: string, reason: string) => void;
  syncBankTransactions: () => Promise<void>;
  updateSettings: (patch: Partial<Settings>) => void;
  setClientRule: (clientId: string, rule: string) => void;
}

const LOAD_TIMEOUT_MS = 15_000;

function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  return new Promise<T>((resolve, reject) => {
    const timer = setTimeout(() => reject(new Error("timeout")), ms);
    promise.then(
      (value) => {
        clearTimeout(timer);
        resolve(value);
      },
      (error) => {
        clearTimeout(timer);
        reject(error);
      },
    );
  });
}

const LedgerContext = createContext<Ctx | null>(null);

export function LedgerProvider({
  platform,
  children,
}: {
  platform: AuthPlatform;
  children: ReactNode;
}) {
  const session = useAuthSession(platform);
  const [ready, setReady] = useState(false);
  const [loadError, setLoadError] = useState(false);
  const [bootKey, setBootKey] = useState(0);
  const [clients, setClients] = useState<Client[]>(EMPTY_LEDGER_STATE.clients);
  const [invoices, setInvoices] = useState<Invoice[]>(EMPTY_LEDGER_STATE.invoices);
  const [transactions, setTransactions] = useState<BankTx[]>(EMPTY_LEDGER_STATE.transactions);
  const [queries, setQueries] = useState<ClientQuery[]>(EMPTY_LEDGER_STATE.queries);
  const [activity, setActivity] = useState<ActivityEvent[]>(EMPTY_LEDGER_STATE.activity);
  const [settings, setSettings] = useState<Settings>(EMPTY_LEDGER_STATE.settings);

  const applyState = useCallback(
    (state: {
      clients: Client[];
      invoices: Invoice[];
      transactions: BankTx[];
      queries: ClientQuery[];
      activity: ActivityEvent[];
      settings: Settings;
    }) => {
      setClients(state.clients);
      setInvoices(state.invoices);
      setTransactions(state.transactions);
      setQueries(state.queries);
      setActivity(state.activity);
      setSettings(state.settings);
    },
    [],
  );

  const refreshState = useCallback(async () => {
    if (!session?.email) return;
    const state = await fetchLedgerState(platform, session.email);
    applyState(state);
  }, [applyState, platform, session?.email]);

  useEffect(() => {
    if (!isApiEnabled()) {
      setLoadError(true);
      setReady(true);
      return;
    }

    if (!session?.email) {
      applyState(EMPTY_LEDGER_STATE);
      setLoadError(false);
      setReady(true);
      return;
    }

    let cancelled = false;

    (async () => {
      try {
        const healthy = await withTimeout(checkApiHealth(), LOAD_TIMEOUT_MS);
        if (cancelled) return;

        if (!healthy) {
          setLoadError(true);
          setReady(true);
          return;
        }

        const state = await withTimeout(fetchLedgerState(platform, session.email), LOAD_TIMEOUT_MS);
        if (cancelled) return;
        applyState(state);
        setLoadError(false);
      } catch {
        if (!cancelled) setLoadError(true);
      } finally {
        if (!cancelled) setReady(true);
      }
    })();

    return () => {
      cancelled = true;
    };
  }, [applyState, bootKey, platform, session?.email]);

  useEffect(() => {
    if (!session?.email || !isApiEnabled()) return;
    const poll = window.setInterval(() => {
      void fetchLedgerState(platform, session.email)
        .then(applyState)
        .catch(() => undefined);
    }, 15_000);
    const onFocus = () => {
      void fetchLedgerState(platform, session.email)
        .then(applyState)
        .catch(() => undefined);
    };
    window.addEventListener("focus", onFocus);
    return () => {
      window.clearInterval(poll);
      window.removeEventListener("focus", onFocus);
    };
  }, [applyState, platform, session?.email]);

  const clientName = useCallback(
    (id: string) => clients.find((c) => c.id === id)?.name ?? "Unknown client",
    [clients],
  );

  const createClient = useCallback(
    async (
      payload: {
        name: string;
        companyName?: string;
        email: string;
        password: string;
        kvk?: string;
      },
      actor: FirmActor,
    ) => {
      const client = await createClientApi(payload, actor);
      await refreshState();
      return client;
    },
    [refreshState],
  );

  const uploadDocumentFile = useCallback(
    async (
      file: File,
      options: {
        platform: AuthPlatform;
        email: string;
        clientId?: string;
        actor: string;
        forceDuplicate?: boolean;
        onProgress?: (percent: number) => void;
      },
    ): Promise<Invoice> => {
      if (!options.clientId) {
        throw new Error("Please select a client");
      }

      try {
        const invoice = await uploadDocumentFileApi(file, {
          platform: options.platform,
          email: options.email,
          clientId: options.clientId,
          actor: options.actor,
          forceDuplicate: options.forceDuplicate,
          onProgress: options.onProgress,
        });
        await refreshState();
        if (!options.forceDuplicate) {
          toast.success("Document uploaded", { description: file.name });
        }
        return invoice;
      } catch (err) {
        if (err instanceof DuplicateUploadError) {
          throw err;
        }
        toast.error(err instanceof Error ? err.message : "Upload failed");
        throw err;
      }
    },
    [refreshState],
  );

  const firmActor = useCallback(() => {
    return {
      platform: platform as AuthPlatform,
      email: session?.email ?? "",
      actor: session?.name ?? "",
    };
  }, [platform, session?.email, session?.name]);

  const setDuplicateStatus = useCallback(
    (id: string, status: string) => {
      void setDuplicateStatusApi(id, status, firmActor())
        .then(() => refreshState())
        .then(() => toast.success("Duplicate status updated"))
        .catch((err: Error) => toast.error(err.message || "Could not update duplicate status"));
    },
    [firmActor, refreshState],
  );

  const updateInvoiceFields = useCallback(
    (id: string, patch: Partial<Invoice>) => {
      void updateInvoiceApi(id, patch, firmActor())
        .then(() => refreshState())
        .then(() => toast.success("Changes saved"))
        .catch((err: Error) => toast.error(err.message || "Could not save invoice changes"));
    },
    [firmActor, refreshState],
  );

  const updateSuggestion = useCallback(
    (id: string, patch: Partial<Suggestion>) => {
      void updateSuggestionApi(id, patch, firmActor())
        .then(() => refreshState())
        .then(() => toast.success("Changes saved"))
        .catch((err: Error) => toast.error(err.message || "Could not save suggestion changes"));
    },
    [firmActor, refreshState],
  );

  const setInvoiceStatus = useCallback(
    (id: string, status: InvoiceStatus) => {
      void setInvoiceStatusApi(id, status, firmActor())
        .then(() => refreshState())
        .catch((err: Error) => toast.error(err.message || "Could not update invoice status"));
    },
    [firmActor, refreshState],
  );

  const approveInvoice = useCallback(
    (id: string) => {
      void approveInvoiceApi(id, firmActor())
        .then(() => refreshState())
        .then(() => toast.success("Document approved successfully."))
        .catch((err: Error) => toast.error(err.message || "Could not approve invoice"));
    },
    [firmActor, refreshState],
  );

  const rejectInvoice = useCallback(
    (id: string, reason: string) => {
      void rejectInvoiceApi(id, reason, firmActor())
        .then(() => refreshState())
        .then(() => toast.success("Document rejected"))
        .catch((err: Error) => toast.error(err.message || "Could not reject invoice"));
    },
    [firmActor, refreshState],
  );

  const syncInvoice = useCallback(
    (id: string) => {
      toast.loading("Syncing…", { id: `sync-${id}` });
      void syncInvoiceApi(id, firmActor())
        .then(async () => {
          await refreshState();
          toast.success("Synced to SnelStart", { id: `sync-${id}` });
        })
        .catch((err: Error) => {
          toast.error("SnelStart sync unavailable", {
            id: `sync-${id}`,
            description: err.message || "SnelStart is not connected for this client.",
          });
        });
    },
    [firmActor, refreshState],
  );

  const bulkApprove = useCallback(
    (ids: string[]) => {
      void bulkApproveApi(ids, firmActor())
        .then(() => refreshState())
        .then(() => toast.success(`${ids.length} invoices approved`))
        .catch((err: Error) => toast.error(err.message || "Bulk approve failed"));
    },
    [firmActor, refreshState],
  );

  const bulkNeedsReview = useCallback(
    (ids: string[]) => {
      void bulkNeedsReviewApi(ids, firmActor())
        .then(() => refreshState())
        .then(() => toast(`${ids.length} invoices marked as needs review`))
        .catch((err: Error) => toast.error(err.message || "Bulk update failed"));
    },
    [firmActor, refreshState],
  );

  const createQuery = useCallback(
    (
      clientId: string,
      question: string,
      opts?: { invoiceId?: string; title?: string; priority?: string },
    ) => {
      void createQueryApi(clientId, question, firmActor(), opts)
        .then(() => refreshState())
        .then(() => toast.success("Question sent to client"))
        .catch((err: Error) => toast.error(err.message || "Could not send question"));
    },
    [firmActor, refreshState],
  );

  const answerQuery = useCallback(
    async (id: string, answer: string, proof?: string) => {
      try {
        await answerQueryApi(id, answer, proof, {
          platform,
          email: session?.email ?? "",
        });
        await refreshState();
        toast.success("Answer sent to your accountant");
      } catch (err) {
        toast.error(err instanceof Error ? err.message : "Could not send answer");
        throw err;
      }
    },
    [platform, refreshState, session?.email],
  );

  const replyToQuery = useCallback(
    async (id: string, message: string, attachmentRef?: string) => {
      const actorSession =
        platform === "firm"
          ? { platform, email: session?.email ?? "", actor: session?.name ?? "" }
          : { platform, email: session?.email ?? "" };
      try {
        await addQueryMessageApi(id, message, actorSession, attachmentRef);
        await refreshState();
        toast.success("Reply sent");
      } catch (err) {
        toast.error(err instanceof Error ? err.message : "Could not send reply");
        throw err;
      }
    },
    [platform, refreshState, session?.email, session?.name],
  );

  const resolveQuery = useCallback(
    async (id: string) => {
      try {
        await resolveQueryApi(id, firmActor());
        await refreshState();
        toast.success("Question resolved");
      } catch (err) {
        toast.error(err instanceof Error ? err.message : "Could not resolve question");
        throw err;
      }
    },
    [firmActor, refreshState],
  );

  const cancelQuery = useCallback(
    async (id: string, reason?: string) => {
      try {
        await cancelQueryApi(id, firmActor(), reason);
        await refreshState();
        toast.success("Question cancelled");
      } catch (err) {
        toast.error(err instanceof Error ? err.message : "Could not cancel question");
        throw err;
      }
    },
    [firmActor, refreshState],
  );

  const approveQuery = useCallback(
    (id: string) => {
      void resolveQueryApi(id, firmActor())
        .then(() => refreshState())
        .then(() => toast.success("Question resolved"))
        .catch((err: Error) => toast.error(err.message || "Could not resolve question"));
    },
    [firmActor, refreshState],
  );

  const setTxStatus = useCallback(
    (id: string, status: BankTx["status"]) => {
      void setTxStatusApi(id, status, firmActor())
        .then(() => refreshState())
        .then(() => toast.success(`Transaction ${status.toLowerCase()}`))
        .catch((err: Error) => toast.error(err.message || "Could not update transaction"));
    },
    [firmActor, refreshState],
  );

  const approveBankMatch = useCallback(
    (txId: string, invoiceId: string, amount?: number) => {
      void approveBankMatchApi(txId, invoiceId, firmActor(), amount)
        .then(() => refreshState())
        .then(() => toast.success("Match approved"))
        .catch((err: Error) => toast.error(err.message || "Could not approve match"));
    },
    [firmActor, refreshState],
  );

  const rejectBankMatch = useCallback(
    (txId: string) => {
      void rejectBankMatchApi(txId, firmActor())
        .then(() => refreshState())
        .then(() => toast.success("Suggestion rejected"))
        .catch((err: Error) => toast.error(err.message || "Could not reject suggestion"));
    },
    [firmActor, refreshState],
  );

  const ignoreBankTransaction = useCallback(
    (txId: string, reason: string) => {
      void ignoreBankTransactionApi(txId, reason, firmActor())
        .then(() => refreshState())
        .then(() => toast.success("Transaction ignored"))
        .catch((err: Error) => toast.error(err.message || "Could not ignore transaction"));
    },
    [firmActor, refreshState],
  );

  const syncBankTransactions = useCallback(() => {
    return syncBankTransactionsApi(firmActor())
      .then((res) => refreshState().then(() => res))
      .then((res) => {
        if (res.imported > 0) {
          toast.success(`Imported ${res.imported} bank transaction(s)`);
          return;
        }
        toast.message("SnelStart import", {
          description: res.message,
        });
      })
      .catch((err: Error) => toast.error(err.message || "Bank sync failed"));
  }, [firmActor, refreshState]);

  const updateSettings = useCallback(
    (patch: Partial<Settings>) => {
      void updateSettingsApi(patch)
        .then(() => refreshState())
        .catch(() => toast.error("Could not update settings"));
    },
    [refreshState],
  );

  const setClientRule = useCallback(
    (clientId: string, rule: string) => {
      void setClientRuleApi(clientId, rule)
        .then(() => refreshState())
        .then(() => toast.success("Rule saved"))
        .catch(() => toast.error("Could not save rule"));
    },
    [refreshState],
  );

  const value = useMemo<Ctx>(
    () => ({
      ready,
      loadError,
      clients,
      invoices,
      transactions,
      queries,
      activity,
      settings,
      clientName,
      getInvoice: (id: string) => invoices.find((i) => i.id === id),
      refreshState,
      createClient,
      uploadDocumentFile,
      updateInvoiceFields,
      updateSuggestion,
      setInvoiceStatus,
      approveInvoice,
      rejectInvoice,
      syncInvoice,
      bulkApprove,
      bulkNeedsReview,
      createQuery,
      answerQuery,
      replyToQuery,
      resolveQuery,
      cancelQuery,
      approveQuery,
      setTxStatus,
      approveBankMatch,
      rejectBankMatch,
      ignoreBankTransaction,
      syncBankTransactions,
      updateSettings,
      setClientRule,
      setDuplicateStatus,
    }),
    [
      activity,
      answerQuery,
      approveInvoice,
      approveQuery,
      bulkApprove,
      bulkNeedsReview,
      cancelQuery,
      clientName,
      clients,
      createClient,
      createQuery,
      invoices,
      loadError,
      queries,
      ready,
      refreshState,
      rejectInvoice,
      replyToQuery,
      resolveQuery,
      setClientRule,
      setDuplicateStatus,
      setInvoiceStatus,
      setTxStatus,
      settings,
      syncInvoice,
      transactions,
      updateInvoiceFields,
      updateSettings,
      updateSuggestion,
      uploadDocumentFile,
    ],
  );

  return (
    <LedgerContext.Provider value={value}>
      {!ready ? (
        <WorkspaceSplash />
      ) : loadError ? (
        <WorkspaceSplash
          status="error"
          onRetry={() => {
            setReady(false);
            setLoadError(false);
            setBootKey((key) => key + 1);
          }}
        />
      ) : (
        children
      )}
    </LedgerContext.Provider>
  );
}

export function useLedger() {
  const ctx = useContext(LedgerContext);
  if (!ctx) throw new Error("useLedger must be used within LedgerProvider");
  return ctx;
}

export const eur = (n: number) =>
  new Intl.NumberFormat("en-IE", { style: "currency", currency: "EUR" }).format(n);

export function relativeTime(ts: number) {
  const diff = Math.round((Date.now() - ts) / 60000);
  if (diff < 1) return "just now";
  if (diff < 60) return `${diff} min ago`;
  const hours = Math.round(diff / 60);
  if (hours < 24) return `${hours} h ago`;
  return `${Math.round(hours / 24)} d ago`;
}
