"use client";

import { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { LayoutGrid, List, Plus, Search } from "lucide-react";
import {
  matchesSearch,
  sortPortfolios,
  statusDisplay,
  upsertLocalConfig,
  type HomeData,
  type HomeSort,
  type PortfolioWire,
} from "@/app/lib/portfolioHomeView";
import FolderRail, { type RailView } from "./FolderRail";
import {
  PortfolioGridCard,
  PortfolioTable,
  type CardActions,
} from "./PortfolioCard";
import NewPortfolioModal from "./NewPortfolioModal";

const HOME_KEY = ["portfolio-home"];

// "Show more" batch size: bounds the initial wall without pagination chrome.
// Divisible by 2 and 3 so grid rows stay full at every breakpoint.
const PAGE_SIZE = 24;

async function fetchHome(): Promise<HomeData> {
  const res = await fetch("/api/me/portfolio-home");
  if (!res.ok) throw new Error("Failed to load portfolios");
  return res.json();
}

async function send(url: string, method: string, body?: unknown) {
  const res = await fetch(url, {
    method,
    headers: body ? { "content-type": "application/json" } : undefined,
    body: body ? JSON.stringify(body) : undefined,
  });
  if (!res.ok) {
    const payload = await res.json().catch(() => null);
    throw new Error(payload?.error ?? "Request failed");
  }
}

// Shared optimistic-mutation plumbing: apply the local update immediately,
// roll back on error, reconcile with the server afterwards.
function useOptimisticHomeMutation<V>(
  mutationFn: (vars: V) => Promise<void>,
  localUpdate: (home: HomeData, vars: V) => HomeData,
  announceError: (message: string) => void,
) {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn,
    onMutate: async (vars: V) => {
      await queryClient.cancelQueries(HOME_KEY);
      const previous = queryClient.getQueryData<HomeData>(HOME_KEY);
      if (previous) {
        queryClient.setQueryData(HOME_KEY, localUpdate(previous, vars));
      }
      return { previous };
    },
    onError: (_err: unknown, _vars: V, context?: { previous?: HomeData }) => {
      if (context?.previous) {
        queryClient.setQueryData(HOME_KEY, context.previous);
      }
      announceError("Something went wrong — change undone");
    },
    onSettled: () => queryClient.invalidateQueries(HOME_KEY),
  });
}

function viewToUrl(view: RailView): string {
  if (view === "starred") return "/home?view=starred";
  if (typeof view === "object") return `/home?folder=${view.folderId}`;
  return "/home";
}

export default function PortfolioHome({
  initialDisplay = "grid",
  initialView = "all",
}: {
  initialDisplay?: "grid" | "list";
  initialView?: RailView;
}) {
  const queryClient = useQueryClient();
  const { data, isLoading, isError, refetch } = useQuery(HOME_KEY, fetchHome);

  const [view, setView] = useState<RailView>(initialView);
  const [display, setDisplay] = useState<"grid" | "list">(initialDisplay);
  const chooseDisplay = (value: "grid" | "list") => {
    setDisplay(value);
    document.cookie = `home-display=${value}; path=/; max-age=31536000; samesite=lax`;
  };
  const [sort, setSort] = useState<HomeSort>("updated");
  const [query, setQuery] = useState("");
  const [visibleCount, setVisibleCount] = useState(PAGE_SIZE);
  // Changing what's listed resets how much of it is revealed. The selection
  // also lands in the URL (replaceState — no navigation) so refresh and
  // sharing keep the folder context.
  const chooseView = (next: RailView) => {
    setView(next);
    setVisibleCount(PAGE_SIZE);
    window.history.replaceState(null, "", viewToUrl(next));
  };
  const chooseQuery = (next: string) => {
    setQuery(next);
    setVisibleCount(PAGE_SIZE);
  };
  const [announcement, setAnnouncement] = useState("");
  const [errorNotice, setErrorNotice] = useState("");
  const [newPortfolioOpen, setNewPortfolioOpen] = useState(false);
  // Failures must be seen, not just announced: feed the sr-only live region
  // AND a visible notice.
  const announceError = (message: string) => {
    setAnnouncement(message);
    setErrorNotice(message);
  };

  const configMutation = useOptimisticHomeMutation(
    (vars: {
      portfolioId: string;
      change: { starred?: boolean; folderId?: string | null };
    }) =>
      send(`/api/me/portfolios/${vars.portfolioId}/config`, "PATCH", vars.change),
    (home, vars) => ({
      ...home,
      configs: upsertLocalConfig(home.configs, vars.portfolioId, {
        ...(vars.change.starred !== undefined && {
          starredAt: vars.change.starred ? new Date().toISOString() : null,
        }),
        ...(vars.change.folderId !== undefined && {
          folderId: vars.change.folderId,
        }),
      }),
    }),
    announceError,
  );

  const reorderMutation = useOptimisticHomeMutation(
    (orderedIds: string[]) =>
      send("/api/me/folders/reorder", "PATCH", { orderedIds }),
    (home, orderedIds) => ({
      ...home,
      folders: home.folders.map((f) => ({
        ...f,
        position: orderedIds.indexOf(f.id),
      })),
    }),
    announceError,
  );

  const renameMutation = useOptimisticHomeMutation(
    (vars: { folderId: string; name: string }) =>
      send(`/api/me/folders/${vars.folderId}`, "PATCH", { name: vars.name }),
    (home, vars) => ({
      ...home,
      folders: home.folders.map((f) =>
        f.id === vars.folderId ? { ...f, name: vars.name } : f,
      ),
    }),
    announceError,
  );

  const deleteMutation = useOptimisticHomeMutation(
    (folderId: string) => send(`/api/me/folders/${folderId}`, "DELETE"),
    (home, folderId) => ({
      ...home,
      folders: home.folders.filter((f) => f.id !== folderId),
      configs: home.configs.map((c) =>
        c.folderId === folderId ? { ...c, folderId: null } : c,
      ),
    }),
    announceError,
  );

  const createMutation = useMutation({
    mutationFn: (name: string) => send("/api/me/folders", "POST", { name }),
    onError: () => announceError("Couldn\u2019t create the folder \u2014 try again"),
    onSettled: () => queryClient.invalidateQueries(HOME_KEY),
  });

  if (isLoading) {
    return (
      <div className="mx-auto flex max-w-[1320px] gap-7 px-6 py-8" aria-busy>
        <div className="hidden w-[230px] shrink-0 animate-pulse rounded-xl bg-gray-200/60 lg:block lg:h-64" />
        <div className="grid flex-1 grid-cols-1 gap-3.5 sm:grid-cols-2 xl:grid-cols-3">
          {Array.from({ length: 6 }, (_, i) => (
            <div key={i} className="h-44 animate-pulse rounded-xl bg-gray-200/60" />
          ))}
        </div>
      </div>
    );
  }
  if (isError || !data) {
    return (
      <div className="mx-auto max-w-[1320px] px-6 py-16 text-center text-gray-600">
        Couldn’t load your portfolios.{" "}
        <button
          type="button"
          onClick={() => refetch()}
          className="font-semibold text-brandmidblue hover:underline"
        >
          Try again
        </button>
      </div>
    );
  }

  // ---- derived render state (all plain derivation, no memoization needed
  // at this scale) ----
  const configOf = new Map(data.configs.map((c) => [c.portfolioId, c]));
  const folderById = new Map(data.folders.map((f) => [f.id, f]));
  const orderedFolders = [...data.folders].sort(
    (a, b) => a.position - b.position,
  );

  // A folder id from the URL or a just-deleted folder may not exist; fall
  // back to All rather than rendering a ghost view.
  const activeView: RailView =
    typeof view === "object" && !folderById.has(view.folderId) ? "all" : view;

  const isStarred = (id: string) => configOf.get(id)?.starredAt != null;
  const folderIdOf = (id: string) => configOf.get(id)?.folderId ?? null;
  const folderNameOf = (id: string) => {
    const folderId = folderIdOf(id);
    return folderId ? (folderById.get(folderId)?.name ?? null) : null;
  };
  const needsAttention = (p: PortfolioWire) =>
    statusDisplay(p.status).needsAttention;

  const inView = (p: PortfolioWire) =>
    activeView === "all"
      ? true
      : activeView === "starred"
        ? isStarred(p.id)
        : folderIdOf(p.id) === activeView.folderId;

  const scope = data.portfolios.filter(inView);
  const filtered = scope.filter((p) =>
    matchesSearch({ ...p, folderName: folderNameOf(p.id) }, query),
  );
  let rows = sortPortfolios(filtered, sort);
  if (activeView === "starred") {
    rows = [...rows].sort(
      (a, b) =>
        new Date(configOf.get(b.id)!.starredAt!).getTime() -
        new Date(configOf.get(a.id)!.starredAt!).getTime(),
    );
  } else {
    rows = [...rows].sort(
      (a, b) => Number(isStarred(b.id)) - Number(isStarred(a.id)),
    );
  }

  const visibleRows = rows.slice(0, visibleCount);
  const hiddenCount = rows.length - visibleRows.length;

  const viewTitle =
    activeView === "all"
      ? "All portfolios"
      : activeView === "starred"
        ? "Starred"
        : (folderById.get(activeView.folderId)?.name ?? "Folder");
  const attentionCount = rows.filter(needsAttention).length;
  const propertiesSum = rows.reduce(
    (sum, p) => sum + (p.numberOfProperties ?? 0),
    0,
  );

  const cardActions: CardActions = {
    isStarred,
    folderIdOf,
    folders: orderedFolders,
    onToggleStar: (p) => {
      const starred = !isStarred(p.id);
      configMutation.mutate({ portfolioId: p.id, change: { starred } });
      setAnnouncement(`${starred ? "Starred" : "Unstarred"} ${p.name}`);
    },
    onMoveToFolder: (p, folderId) => {
      configMutation.mutate({ portfolioId: p.id, change: { folderId } });
      setAnnouncement(
        folderId
          ? `Moved ${p.name} to ${folderById.get(folderId)?.name}`
          : `Removed ${p.name} from its folder`,
      );
    },
  };

  return (
    <div className="mx-auto max-w-[1320px] px-6 py-7">
      <div className="mb-6 flex flex-wrap items-center gap-3">
        <h1 className="text-2xl font-bold tracking-tight text-gray-900">
          Portfolios
        </h1>
        <div className="ml-auto flex flex-wrap items-center gap-2.5">
          <label className="relative">
            <Search
              size={14}
              className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"
              aria-hidden
            />
            <input
              type="search"
              value={query}
              onChange={(e) => chooseQuery(e.target.value)}
              placeholder="Search portfolios"
              aria-label="Search portfolios by name, status, goal or folder"
              className="w-60 rounded-lg border border-[#c3ccd8] bg-white py-2 pl-8 pr-3 text-sm placeholder:text-gray-500"
            />
          </label>
          <select
            value={sort}
            onChange={(e) => setSort(e.target.value as HomeSort)}
            aria-label="Sort portfolios"
            className="rounded-lg border border-[#c3ccd8] bg-white px-2.5 py-2 text-sm"
          >
            <option value="updated">Recently updated</option>
            <option value="attention">Needs attention first</option>
            <option value="name">Name A–Z</option>
            <option value="props">Most properties</option>
          </select>
          <div
            role="group"
            aria-label="View"
            className="flex overflow-hidden rounded-lg border border-[#c3ccd8] bg-white"
          >
            {(
              [
                ["grid", LayoutGrid, "Grid view"],
                ["list", List, "List view"],
              ] as const
            ).map(([value, Icon, label]) => (
              <button
                key={value}
                type="button"
                aria-pressed={display === value}
                aria-label={label}
                title={label}
                onClick={() => chooseDisplay(value)}
                className={`grid place-items-center px-2.5 py-2 ${
                  display === value
                    ? "bg-brandblue text-white"
                    : "text-gray-500 hover:text-gray-900"
                }`}
              >
                <Icon size={15} aria-hidden />
              </button>
            ))}
          </div>
          <button
            type="button"
            onClick={() => setNewPortfolioOpen(true)}
            className="flex items-center gap-1.5 rounded-lg bg-brandblue px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-[#232866]"
          >
            <Plus size={15} aria-hidden /> New portfolio
          </button>
        </div>
      </div>

      {errorNotice && (
        <div className="mb-4 flex items-center gap-3 rounded-lg border border-red-200 bg-red-50 px-4 py-2.5 text-sm text-red-800">
          <span className="flex-1">{errorNotice}</span>
          <button
            type="button"
            onClick={() => setErrorNotice("")}
            className="rounded-md px-2 py-0.5 font-semibold hover:bg-red-100"
          >
            Dismiss
          </button>
        </div>
      )}

      <div className="flex flex-col gap-4 lg:flex-row lg:gap-7">
        <FolderRail
          folders={orderedFolders}
          view={activeView}
          totalCount={data.portfolios.length}
          starredCount={data.portfolios.filter((p) => isStarred(p.id)).length}
          attentionTotal={data.portfolios.filter(needsAttention).length}
          countOf={(folderId) =>
            data.portfolios.filter((p) => folderIdOf(p.id) === folderId).length
          }
          attentionOf={(folderId) =>
            data.portfolios.filter(
              (p) => folderIdOf(p.id) === folderId && needsAttention(p),
            ).length
          }
          onSelect={chooseView}
          onReorder={(orderedIds) => {
            reorderMutation.mutate(orderedIds);
            setAnnouncement("Folder order saved");
          }}
          onCreate={(name) => createMutation.mutate(name)}
          onDropToFolder={(portfolioId, folderId) => {
            const portfolio = data.portfolios.find((p) => p.id === portfolioId);
            if (!portfolio || folderIdOf(portfolioId) === folderId) return;
            configMutation.mutate({ portfolioId, change: { folderId } });
            setAnnouncement(
              folderId
                ? `Moved ${portfolio.name} to ${folderById.get(folderId)?.name}`
                : `Removed ${portfolio.name} from its folder`,
            );
          }}
          onDropToStarred={(portfolioId) => {
            const portfolio = data.portfolios.find((p) => p.id === portfolioId);
            if (!portfolio || isStarred(portfolioId)) return;
            configMutation.mutate({ portfolioId, change: { starred: true } });
            setAnnouncement(`Starred ${portfolio.name}`);
          }}
          onRename={(folderId, name) =>
            renameMutation.mutate({ folderId, name })
          }
          onDelete={(folderId) => {
            if (typeof activeView === "object" && activeView.folderId === folderId) {
              chooseView("all");
            }
            deleteMutation.mutate(folderId);
            setAnnouncement("Folder removed — its portfolios are unfiled");
          }}
        />

        <main className="min-w-0 flex-1">
          <div className="mb-4">
            <h2 className="text-[17px] font-semibold tracking-tight text-gray-900">
              {viewTitle}
            </h2>
            <div className="text-[13px] text-gray-500">
              {query.trim() ? (
                <>
                  Showing <span className="tabular-nums">{rows.length}</span> of{" "}
                  <span className="tabular-nums">{scope.length}</span> for “
                  {query.trim()}”
                </>
              ) : (
                <>
                  <span className="tabular-nums">{rows.length}</span> portfolios
                  {propertiesSum > 0 && (
                    <>
                      {" "}
                      · <span className="tabular-nums">
                        {propertiesSum.toLocaleString()}
                      </span>{" "}
                      properties
                    </>
                  )}
                  {attentionCount > 0 && (
                    <>
                      {" "}
                      ·{" "}
                      <b className="font-semibold text-red-700">
                        {attentionCount} need attention
                      </b>
                    </>
                  )}
                </>
              )}
            </div>
          </div>

          {rows.length === 0 ? (
            query.trim() ? (
              <div className="py-12 text-center text-sm text-gray-500">
                No portfolios in {viewTitle} match “
                <b className="text-gray-900">{query.trim()}</b>”.{" "}
                <button
                  type="button"
                  onClick={() => chooseQuery("")}
                  className="font-semibold text-brandmidblue hover:underline"
                >
                  Clear search
                </button>
              </div>
            ) : (
              <div className="rounded-xl border border-dashed border-[#c3ccd8] px-5 py-6 text-sm text-gray-500">
                {activeView === "starred" ? (
                  <>
                    <b className="font-semibold text-gray-900">
                      Nothing starred yet.
                    </b>{" "}
                    Star a portfolio to keep your day-to-day work one click
                    away.
                  </>
                ) : activeView === "all" ? (
                  <>
                    <b className="font-semibold text-gray-900">
                      No portfolios yet.
                    </b>{" "}
                    Create your first portfolio to get started.
                  </>
                ) : (
                  <>
                    <b className="font-semibold text-gray-900">
                      This folder is empty.
                    </b>{" "}
                    Move portfolios here with the ⋯ menu on any card.
                  </>
                )}
              </div>
            )
          ) : display === "grid" ? (
            <div className="grid grid-cols-1 gap-3.5 sm:grid-cols-2 xl:grid-cols-3">
              {visibleRows.map((portfolio) => (
                <PortfolioGridCard
                  key={portfolio.id}
                  portfolio={portfolio}
                  actions={cardActions}
                  folderName={
                    activeView === "all" || activeView === "starred"
                      ? folderNameOf(portfolio.id)
                      : null
                  }
                />
              ))}
            </div>
          ) : (
            <PortfolioTable
              portfolios={visibleRows}
              actions={cardActions}
              folderNameOf={
                activeView === "all" || activeView === "starred" ? folderNameOf : () => null
              }
            />
          )}
          {hiddenCount > 0 && (
            <div className="mt-5 flex flex-col items-center gap-1">
              <button
                type="button"
                onClick={() => setVisibleCount((count) => count + PAGE_SIZE)}
                className="rounded-lg border border-[#c3ccd8] bg-white px-5 py-2 text-sm font-semibold text-gray-900 hover:border-brandmidblue hover:text-brandmidblue"
              >
                Show {Math.min(PAGE_SIZE, hiddenCount)} more
              </button>
              <span className="text-xs text-gray-500">
                Showing <span className="tabular-nums">{visibleRows.length}</span>{" "}
                of <span className="tabular-nums">{rows.length}</span>
              </span>
            </div>
          )}
        </main>
      </div>

      <div role="status" aria-live="polite" className="sr-only">
        {announcement}
      </div>
      <NewPortfolioModal
        isOpen={newPortfolioOpen}
        setIsOpen={setNewPortfolioOpen}
      />
    </div>
  );
}
