"use client";

import { useState, useMemo, useRef, useEffect, useCallback } from "react";
import { useProperties } from "./useProperties";
import DataTable from "./dataTable";
import PropertyFilters from "./PropertyFilters";
import { FilterGroups } from "@/app/utils/propertyFilters";
import {
  FunnelIcon,
  ChevronDownIcon,
  XMarkIcon,
  ArrowDownTrayIcon,
  ViewColumnsIcon,
  PlusIcon,
  MagnifyingGlassIcon,
  DocumentArrowUpIcon,
  PlayIcon,
} from "@heroicons/react/24/outline";
import { useRouter } from "next/navigation";
import { HomeIcon } from "@heroicons/react/24/outline";
import BulkUploadComingSoonModal from "@/app/components/portfolio/BulkUploadComingSoonModal";
import { sapToEpc } from "@/app/utils";
import {
  columns,
  columnsWithoutSelect,
} from "@/app/portfolio/[slug]/components/propertyTableColumns";
import { PropertyWithRelations } from "@/app/db/schema/property";
import { MAINFUEL_OPTIONS, EnumOption } from "@/app/utils/propertyFilters";
import {
  OPTIONAL_COLUMN_IDS,
  OPTIONAL_COLUMN_LABELS,
} from "@/app/portfolio/[slug]/components/propertyTableColumns";
import {
  VisibilityState,
  Updater,
  PaginationState,
  RowSelectionState,
} from "@tanstack/react-table";
import { Tooltip } from "./Tooltip";
import { TagsMenu } from "./TagsMenu";
import { TagActionBar, TagMode } from "./TagActionBar";
import { BulkTagUploadModal } from "./BulkTagUploadModal";

import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/app/shadcn_components/ui/dropdown-menu";


/* ----------------------------------------
   Export helpers
----------------------------------------- */
const EXPORT_LIMIT = 6000;

// Exactly one navy CTA is primary at a time (impeccable distill): Add properties
// leads an empty portfolio, Run modelling leads a populated one; the other demotes
// to the neutral toolbar-button style so the toolbar has a single focal action.
const PRIMARY_CTA =
  "flex items-center gap-1.5 h-8 px-3 rounded-lg bg-primary text-white text-xs font-semibold hover:opacity-90 transition";
const SECONDARY_CTA =
  "flex items-center gap-1.5 h-8 px-3 rounded-lg border border-slate-200 bg-white text-primary text-xs font-semibold hover:bg-slate-50 transition";

function resolveEnumLabel(
  options: EnumOption[],
  dbValue: string | null | undefined,
): string {
  if (dbValue == null) return "";
  const opt = options.find((o) => o.dbValues.includes(dbValue));
  return opt?.label ?? dbValue;
}

function exportToCsv(data: PropertyWithRelations[]) {
  const headers = [
    "Address",
    "Postcode",
    "Property Ref",
    "Current EPC",
    "Lodged EPC",
    "Expected EPC",
    "EPC Expiry",
    "EPC Expired",
    "Plan Cost (£)",
    "Property Type",
    "Built Form",
    "Construction Age",
    "Floor Area (m²)",
    "CO₂ Emissions (kg/m²/yr)",
    "Main Fuel",
    "Wall Type",
    "Roof Type",
    "Heating System",
  ];

  const rows = data.map((p) => {
    const lodgedLetter = p.originalSapPoints
      ? (sapToEpc(p.originalSapPoints) ?? "")
      : "";
    const expectedSap =
      (p.currentSapPoints ?? 0) + (p.totalRecommendationSapPoints ?? 0);
    const expectedLetter = expectedSap > 0 ? (sapToEpc(expectedSap) ?? "") : "";

    let expiryStr = "";
    if (p.epcLodgementDate) {
      const expiry = new Date(p.epcLodgementDate);
      expiry.setFullYear(expiry.getFullYear() + 10);
      expiryStr = expiry.toLocaleDateString("en-GB");
    }

    return [
      p.address ?? "",
      p.postcode ?? "",
      p.landlordPropertyId ?? "",
      p.currentEpcRating ?? "",
      lodgedLetter,
      expectedLetter,
      expiryStr,
      p.epcIsExpired ? "Yes" : "No",
      p.totalRecommendationCost ? p.totalRecommendationCost.toFixed(2) : "",
      p.propertyType ?? "",
      p.builtForm ?? "",
      p.yearBuilt ?? "",
      p.totalFloorArea != null ? p.totalFloorArea.toFixed(1) : "",
      p.co2Emissions != null ? p.co2Emissions.toFixed(1) : "",
      resolveEnumLabel(MAINFUEL_OPTIONS, p.mainfuel),
      p.wallType ?? "",
      p.roofType ?? "",
      p.heatingSystem ?? "",
    ];
  });

  const csv = [headers, ...rows]
    .map((row) =>
      row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(","),
    )
    .join("\n");

  const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = "properties.csv";
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
}

/* ----------------------------------------
   Empty portfolio state
----------------------------------------- */
function EmptyPropertyState() {
  return (
    <div className="flex justify-center py-16">
      <div className="text-center text-gray-400">
        <HomeIcon className="h-16 w-16 mx-auto mb-4 text-gray-200" />
        <p>
          Use <strong>&ldquo;Add properties&rdquo;</strong> to start adding
          properties to your portfolio.
        </p>
      </div>
    </div>
  );
}

/* ----------------------------------------
   Loading overlay
----------------------------------------- */
function LoadingOverlay() {
  return (
    <div className="absolute inset-0 z-10 rounded-xl bg-white/60 flex items-center justify-center pointer-events-none">
      <div className="flex flex-col items-center gap-2">
        <div className="h-6 w-6 animate-spin rounded-full border-2 border-gray-200 border-t-gray-700" />
        <span className="text-xs text-gray-500">Updating…</span>
      </div>
    </div>
  );
}

/* ----------------------------------------
   Quick filter dropdown button
----------------------------------------- */
type QuickFilterKey = "address" | "postcode" | "propertyRef";

interface QuickFilterDropdownProps {
  label: string;
  placeholder: string;
  committedValue: string;
  isOpen: boolean;
  onOpen: () => void;
  onCommit: (value: string) => void;
  onClear: () => void;
  inputWidth?: string;
}

function QuickFilterDropdown({
  label,
  placeholder,
  committedValue,
  isOpen,
  onOpen,
  onCommit,
  onClear,
  inputWidth = "w-52",
}: QuickFilterDropdownProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLInputElement>(null);
  const [draft, setDraft] = useState(committedValue);

  useEffect(() => {
    if (isOpen) {
      setDraft(committedValue);
      setTimeout(() => inputRef.current?.focus(), 0);
    }
  }, [isOpen, committedValue]);

  const commit = useCallback(() => {
    onCommit(draft.trim());
  }, [draft, onCommit]);

  useEffect(() => {
    if (!isOpen) return;
    function handleMouseDown(e: MouseEvent) {
      if (
        containerRef.current &&
        !containerRef.current.contains(e.target as Node)
      ) {
        commit();
      }
    }
    document.addEventListener("mousedown", handleMouseDown);
    return () => document.removeEventListener("mousedown", handleMouseDown);
  }, [isOpen, commit]);

  const isActive = Boolean(committedValue);

  return (
    <div ref={containerRef} className="relative">
      {/* Pill is a container, not a button — the open trigger and the clear
          control are sibling buttons so no interactive element is nested. */}
      <div
        className={[
          "flex items-center h-8 px-3 rounded-lg border text-xs font-semibold transition shrink-0",
          isActive
            ? "border-brandblue bg-brandblue text-white"
            : "border-slate-200 bg-white text-primary hover:bg-slate-50",
        ].join(" ")}
      >
        <button
          type="button"
          onClick={onOpen}
          className="flex min-w-0 items-center gap-1.5 text-left"
        >
          <span>{label}</span>
          {isActive ? (
            <span className="opacity-75 max-w-[120px] truncate">
              : {committedValue}
            </span>
          ) : (
            <ChevronDownIcon className="h-3.5 w-3.5 opacity-50" />
          )}
        </button>
        {isActive && (
          <button
            type="button"
            onClick={(e) => {
              e.stopPropagation();
              onClear();
            }}
            aria-label={`Clear ${label} filter`}
            className="ml-1 rounded-full hover:opacity-75"
          >
            <XMarkIcon className="h-3.5 w-3.5" />
          </button>
        )}
      </div>

      {isOpen && (
        <div className="absolute left-0 top-full mt-1 z-30 bg-white border border-slate-200 rounded-lg shadow-md p-2 flex gap-1.5 items-center">
          <input
            ref={inputRef}
            className={`h-8 rounded-lg border border-slate-200 px-2.5 text-xs focus:outline-none focus:ring-2 focus:ring-brandblue/20 focus:border-brandblue ${inputWidth}`}
            placeholder={placeholder}
            value={draft}
            onChange={(e) => setDraft(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === "Enter") commit();
              if (e.key === "Escape") {
                setDraft(committedValue);
                onCommit(committedValue);
              }
            }}
          />
          <button
            onClick={commit}
            className="h-8 px-2.5 rounded-lg bg-brandblue text-white text-xs font-semibold hover:opacity-90 transition whitespace-nowrap"
          >
            Apply
          </button>
        </div>
      )}
    </div>
  );
}

/* ----------------------------------------
   Main table
----------------------------------------- */
export default function PropertyTable({
  portfolioId,
}: {
  portfolioId: string;
}) {
  const router = useRouter();
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [bulkImportOpen, setBulkImportOpen] = useState(false);

  const [committedAddress, setCommittedAddress] = useState("");
  const [committedPostcode, setCommittedPostcode] = useState("");
  const [committedPropertyRef, setCommittedPropertyRef] = useState("");
  const [openFilter, setOpenFilter] = useState<QuickFilterKey | null>(null);
  const [filterGroups, setFilterGroups] = useState<FilterGroups>([]);

  // Row selection for bulk tagging — keyed by property id (see DataTable getRowId).
  const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
  const selectedIds = Object.keys(rowSelection).filter((id) => rowSelection[id]);
  // Deliberate "select all matching" escalation (whole set, not just loaded rows).
  const [selectAllMatching, setSelectAllMatching] = useState(false);

  const clearSelection = () => {
    setRowSelection({});
    setSelectAllMatching(false);
  };
  // Any manual checkbox change drops the all-matching escalation back to the
  // explicit (possibly now-smaller) row selection.
  const handleRowSelectionChange = (
    updater: Updater<RowSelectionState>,
  ) => {
    setSelectAllMatching(false);
    setRowSelection(updater);
  };

  // Bulk-tag flows launched from the Tags menu: a contextual assign/remove mode,
  // and the file-upload modal.
  const [tagMode, setTagMode] = useState<TagMode | null>(null);
  const [tagUploadOpen, setTagUploadOpen] = useState(false);

  // Column visibility — lifted up from DataTable
  const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
    () => {
      const init: VisibilityState = {};
      OPTIONAL_COLUMN_IDS.forEach((id) => {
        // Tags ship visible by default (the headline of ADR-0013); the rest hidden.
        init[id] = id === "tags";
      });
      return init;
    },
  );

  function commitFilter(field: QuickFilterKey, value: string) {
    if (field === "address") setCommittedAddress(value);
    if (field === "postcode") setCommittedPostcode(value);
    if (field === "propertyRef") setCommittedPropertyRef(value);
    setOpenFilter(null);
  }

  function clearAll() {
    setCommittedAddress("");
    setCommittedPostcode("");
    setCommittedPropertyRef("");
    setOpenFilter(null);
    setFilterGroups([]);
  }

  const allFilterGroups = useMemo((): FilterGroups => {
    const quick: FilterGroups = [];
    if (committedAddress)
      quick.push({
        id: "qa",
        conditions: [
          {
            id: "qa-c",
            field: "address",
            operator: "contains",
            value: committedAddress,
          },
        ],
      });
    if (committedPostcode)
      quick.push({
        id: "qp",
        conditions: [
          {
            id: "qp-c",
            field: "postcode",
            operator: "starts_with",
            value: committedPostcode,
          },
        ],
      });
    if (committedPropertyRef)
      quick.push({
        id: "qr",
        conditions: [
          {
            id: "qr-c",
            field: "propertyRef",
            operator: "contains",
            value: committedPropertyRef,
          },
        ],
      });
    return [...quick, ...filterGroups];
  }, [committedAddress, committedPostcode, committedPropertyRef, filterGroups]);

  const hasActiveFilters = allFilterGroups.length > 0;

  const {
    data: filteredResponse,
    isLoading,
    isFetching,
    isError,
  } = useProperties({
    portfolioId,
    filterGroups: allFilterGroups,
  });

  const queryData = useMemo(
    () => filteredResponse?.data ?? [],
    [filteredResponse?.data],
  );
  const filteredTotal = filteredResponse?.total ?? 0;

  // Second query for total (no filters) — React Query dedupes when filters are empty
  const { data: allResponse } = useProperties({
    portfolioId,
    filterGroups: [],
  });
  const totalCount = allResponse?.total ?? 0;
  const [modelNudgeDismissed, setModelNudgeDismissed] = useState(false);

  const DISPLAY_LIMIT = 250;
  const LOAD_MORE_SIZE = 100;

  // ── Extra rows (lazy-loaded pages beyond the initial 1 000) ──────────────
  // Keyed to the current filter set so they auto-clear on filter change.
  const filterKey = useMemo(
    () => JSON.stringify(allFilterGroups),
    [allFilterGroups],
  );
  const [extraState, setExtraState] = useState<{
    filterKey: string;
    rows: PropertyWithRelations[];
  }>({ filterKey: "", rows: [] });

  const extraRows = useMemo(
    () => (extraState.filterKey === filterKey ? extraState.rows : []),
    [extraState, filterKey],
  );

  // The full data visible to the table — initial batch + any lazy-loaded rows
  const tableData = useMemo(
    () => (extraRows.length > 0 ? [...queryData, ...extraRows] : queryData),
    [queryData, extraRows],
  );

  // Properties with no default plan yet — the "not modelled" set. Counted from the
  // loaded rows (cheap; avoids a hook per project convention): exact for a
  // fully-loaded portfolio, a floor when more rows lazy-load beyond view.
  const unmodelledInView = tableData.filter((p) => p.planId == null).length;

  // Bulk-select scope: how many properties match the current view, whether every
  // loaded row is ticked, and whether more match than are loaded (so "select all
  // matching" is worth offering). matchingCount is filtered-total when a filter
  // is active, else the whole portfolio.
  const matchingCount = hasActiveFilters ? filteredTotal : totalCount;
  const allLoadedSelected =
    selectedIds.length > 0 && selectedIds.length === tableData.length;
  const moreMatchThanLoaded = tableData.length < matchingCount;

  // The not-modelled banner owns the Run-modelling CTA in this state, so the
  // toolbar must NOT also show one — otherwise two identical navy buttons appear.
  const showModelNudge =
    !isLoading &&
    totalCount > 0 &&
    unmodelledInView > 0 &&
    !modelNudgeDismissed;

  // Controlled pagination (lifted from DataTable so we can detect the last page)
  const [pagination, setPagination] = useState<PaginationState>({
    pageIndex: 0,
    pageSize: 7,
  });

  // Reset to page 1 whenever the filter changes
  const prevFilterKeyRef = useRef(filterKey);
  if (prevFilterKeyRef.current !== filterKey) {
    prevFilterKeyRef.current = filterKey;
    if (pagination.pageIndex !== 0)
      setPagination((p) => ({ ...p, pageIndex: 0 }));
    // The selection is filter-scoped — a changed filter invalidates it.
    if (selectedIds.length > 0 || selectAllMatching) clearSelection();
  }

  const [isFetchingMore, setIsFetchingMore] = useState(false);

  const pageCount = Math.ceil(tableData.length / pagination.pageSize);
  const isOnLastPage = pageCount > 0 && pagination.pageIndex >= pageCount - 1;
  const hasMore = tableData.length < filteredTotal;

  const isAtDisplayLimit =
    queryData.length >= DISPLAY_LIMIT && filteredTotal > DISPLAY_LIMIT;

  const loadMore = useCallback(async () => {
    if (isFetchingMore || !hasMore || isLoading || isFetching) return;
    setIsFetchingMore(true);
    try {
      const res = await fetch("/api/properties", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          portfolioId,
          filters: allFilterGroups,
          offset: tableData.length,
          limit: LOAD_MORE_SIZE,
        }),
      });
      if (!res.ok) return;
      const json: { data: PropertyWithRelations[] } = await res.json();
      setExtraState((prev) => ({
        filterKey,
        rows:
          prev.filterKey === filterKey
            ? [...prev.rows, ...json.data]
            : json.data,
      }));
    } catch {
      // silently ignore — user can navigate away and back to retry
    } finally {
      setIsFetchingMore(false);
    }
  }, [
    isFetchingMore,
    hasMore,
    isLoading,
    isFetching,
    portfolioId,
    allFilterGroups,
    tableData.length,
    filterKey,
  ]);

  useEffect(() => {
    if (isOnLastPage && hasMore) loadMore();
  }, [isOnLastPage, hasMore, loadMore]);

  // Export fetches its own batch (up to EXPORT_LIMIT) for the current filters,
  // independent of the on-screen page — the display fetch is deliberately small.
  const [isExporting, setIsExporting] = useState(false);
  const [exportError, setExportError] = useState<string | null>(null);
  const handleExport = useCallback(async () => {
    if (isExporting) return;
    setIsExporting(true);
    setExportError(null);
    try {
      const res = await fetch("/api/properties", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          portfolioId,
          filters: allFilterGroups,
          limit: EXPORT_LIMIT,
          offset: 0,
        }),
      });
      if (!res.ok) throw new Error();
      const json: { data: PropertyWithRelations[] } = await res.json();
      exportToCsv(json.data);
    } catch {
      // Don't fail silently — a data-trust product must say when an export dies.
      setExportError("Export failed — please try again.");
    } finally {
      setIsExporting(false);
    }
  }, [isExporting, portfolioId, allFilterGroups]);

  return (
    <div className="py-4 mx-4">
      {/* Action bar */}
      <div className="flex items-center justify-between mb-3">
        {/* Left: results count */}
        <div className="flex items-center gap-3">
          <span className="text-xs font-black uppercase tracking-wider text-primary">
            Results:
          </span>
          {isLoading ? (
            <span className="text-xs text-slate-400">Loading…</span>
          ) : (
            <span className="text-xs text-slate-500">
              Showing{" "}
              <span className="font-bold text-primary">
                {tableData.length.toLocaleString()}
              </span>{" "}
              of{" "}
              <span className="font-bold text-primary">
                {totalCount.toLocaleString()}
              </span>{" "}
              properties
            </span>
          )}
          {(selectedIds.length > 0 || selectAllMatching) && (
            <span className="inline-flex items-center gap-1.5 rounded-full border border-brandmidblue/30 bg-brandlightblue/50 px-2 py-0.5 text-xs font-semibold text-brandblue">
              {selectAllMatching
                ? `All ${matchingCount.toLocaleString()} selected`
                : `${selectedIds.length.toLocaleString()} selected`}
              <button
                onClick={clearSelection}
                className="text-brandmidblue hover:text-brandblue"
              >
                Clear
              </button>
            </span>
          )}
          {exportError && (
            <span className="inline-flex items-center gap-1.5 rounded-md border border-red-200 bg-red-50 px-2 py-0.5 text-xs font-medium text-red-700">
              {exportError}
              <button
                onClick={() => setExportError(null)}
                aria-label="Dismiss"
                className="text-red-400 hover:text-red-700"
              >
                <XMarkIcon className="h-3 w-3" />
              </button>
            </span>
          )}
          {hasActiveFilters && (
            <button
              onClick={clearAll}
              className="text-xs text-slate-400 hover:text-primary underline"
            >
              Clear all
            </button>
          )}
        </div>

        {/* Right: action buttons */}
        <div className="flex items-center gap-2">
          {/* Filters toggle */}
          <button
            onClick={() => setSidebarOpen((o) => !o)}
            className={[
              "flex items-center gap-1.5 h-8 px-3 rounded-lg border text-xs font-semibold transition shrink-0",
              sidebarOpen
                ? "bg-primary text-white border-primary"
                : "border-slate-200 bg-white text-primary hover:bg-slate-50",
            ].join(" ")}
            title={sidebarOpen ? "Hide filters" : "Show filters"}
          >
            <FunnelIcon className="h-3.5 w-3.5" />
            Filters
          </button>

          {/* Edit Columns dropdown */}
          <DropdownMenu>
            <DropdownMenuTrigger asChild>
              <button className="flex items-center gap-1.5 h-8 px-3 rounded-lg border border-slate-200 bg-white text-xs font-semibold text-primary hover:bg-slate-50 transition">
                <ViewColumnsIcon className="h-3.5 w-3.5" />
                Edit Columns
              </button>
            </DropdownMenuTrigger>
            <DropdownMenuContent align="end" className="w-48">
              <DropdownMenuLabel className="text-xs">
                Optional Columns
              </DropdownMenuLabel>
              <DropdownMenuSeparator />
              {OPTIONAL_COLUMN_IDS.map((colId) => {
                const isVisible = columnVisibility[colId] !== false;
                return (
                  <DropdownMenuItem
                    key={colId}
                    className="flex items-center gap-2 cursor-pointer"
                    onSelect={(e) => {
                      e.preventDefault();
                      setColumnVisibility((prev) => ({
                        ...prev,
                        [colId]: !isVisible,
                      }));
                    }}
                  >
                    <input
                      type="checkbox"
                      checked={isVisible}
                      readOnly
                      className="h-3.5 w-3.5 accent-brandmidblue"
                    />
                    <span className="text-xs">
                      {OPTIONAL_COLUMN_LABELS[colId]}
                    </span>
                  </DropdownMenuItem>
                );
              })}
            </DropdownMenuContent>
          </DropdownMenu>

          {/* Export */}
          {filteredTotal > EXPORT_LIMIT ? (
            <Tooltip
              content={`Export is limited to ${EXPORT_LIMIT.toLocaleString()} properties. Refine your filters to enable it.`}
            >
              <button
                disabled
                className="flex items-center gap-1.5 h-8 px-3 rounded-lg border border-slate-200 bg-slate-100 text-xs font-semibold text-slate-400 cursor-not-allowed opacity-60"
              >
                <ArrowDownTrayIcon className="h-3.5 w-3.5" />
                Export
                <span className="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full bg-amber-400 text-white text-[9px] font-black leading-none">
                  !
                </span>
              </button>
            </Tooltip>
          ) : (
            <button
              onClick={handleExport}
              disabled={isLoading || isExporting || filteredTotal === 0}
              className="flex items-center gap-1.5 h-8 px-3 rounded-lg border border-slate-200 bg-slate-100 text-xs font-semibold text-primary hover:bg-slate-200 transition disabled:opacity-50 disabled:cursor-not-allowed"
            >
              <ArrowDownTrayIcon className="h-3.5 w-3.5" />
              {isExporting ? "Exporting…" : "Export"}
            </button>
          )}

          {/* Tags — bulk assign / remove / upload (ADR-0013) */}
          <TagsMenu
            onUpload={() => setTagUploadOpen(true)}
            onAssign={() => {
              clearSelection();
              setTagMode("assign");
            }}
            onRemove={() => {
              clearSelection();
              setTagMode("remove");
            }}
          />

          {/* Add properties — primary CTA only while the portfolio is empty */}
          <DropdownMenu>
            <DropdownMenuTrigger asChild>
              <button className={totalCount > 0 ? SECONDARY_CTA : PRIMARY_CTA}>
                <PlusIcon className="h-3.5 w-3.5" />
                Add properties
                <ChevronDownIcon className="h-3.5 w-3.5 opacity-70" />
              </button>
            </DropdownMenuTrigger>
            <DropdownMenuContent align="end" className="w-72 p-1.5">
              <DropdownMenuItem
                className="flex items-start gap-3 cursor-pointer rounded-lg p-3"
                onSelect={() =>
                  router.push(`/portfolio/${portfolioId}/properties/add`)
                }
              >
                <MagnifyingGlassIcon className="h-[18px] w-[18px] mt-0.5 text-gray-700 shrink-0" />
                <span>
                  <span className="block text-sm font-semibold text-gray-900">
                    Search by postcode
                  </span>
                  <span className="block text-xs text-slate-400">
                    Find addresses and add them in a few clicks
                  </span>
                </span>
              </DropdownMenuItem>
              <DropdownMenuItem
                className="flex items-start gap-3 cursor-pointer rounded-lg p-3"
                onSelect={() => setBulkImportOpen(true)}
              >
                <DocumentArrowUpIcon className="h-[18px] w-[18px] mt-0.5 text-gray-700 shrink-0" />
                <span className="flex-1">
                  <span className="block text-sm font-semibold text-gray-900">
                    Bulk excel import
                  </span>
                  <span className="block text-xs text-slate-400">
                    Upload a spreadsheet of addresses
                  </span>
                </span>
              </DropdownMenuItem>
            </DropdownMenuContent>
          </DropdownMenu>

          <BulkUploadComingSoonModal
            isOpen={bulkImportOpen}
            onClose={() => setBulkImportOpen(false)}
            portfolioId={portfolioId}
          />

          {/* Run modelling — primary CTA once there are properties to model.
              Hidden while the not-modelled banner is up, since the banner already
              carries this action (no duplicate navy CTA). */}
          {!showModelNudge && (
            <button
              onClick={() =>
                router.push(`/portfolio/${portfolioId}/modelling/run`)
              }
              className={totalCount > 0 ? PRIMARY_CTA : SECONDARY_CTA}
            >
              <PlayIcon className="h-3.5 w-3.5" />
              Run modelling
            </button>
          )}
        </div>
      </div>

      {/* Quick filters row */}
      <div className="flex items-center gap-2 mb-3 flex-wrap">
        <QuickFilterDropdown
          label="Address"
          placeholder="Contains…"
          committedValue={committedAddress}
          isOpen={openFilter === "address"}
          onOpen={() =>
            setOpenFilter(openFilter === "address" ? null : "address")
          }
          onCommit={(v) => commitFilter("address", v)}
          onClear={() => {
            setCommittedAddress("");
            setOpenFilter(null);
          }}
          inputWidth="w-52"
        />

        <QuickFilterDropdown
          label="Postcode"
          placeholder="e.g. E17"
          committedValue={committedPostcode}
          isOpen={openFilter === "postcode"}
          onOpen={() =>
            setOpenFilter(openFilter === "postcode" ? null : "postcode")
          }
          onCommit={(v) => commitFilter("postcode", v)}
          onClear={() => {
            setCommittedPostcode("");
            setOpenFilter(null);
          }}
          inputWidth="w-32"
        />

        <QuickFilterDropdown
          label="Property Ref"
          placeholder="Landlord ref…"
          committedValue={committedPropertyRef}
          isOpen={openFilter === "propertyRef"}
          onOpen={() =>
            setOpenFilter(openFilter === "propertyRef" ? null : "propertyRef")
          }
          onCommit={(v) => commitFilter("propertyRef", v)}
          onClear={() => {
            setCommittedPropertyRef("");
            setOpenFilter(null);
          }}
          inputWidth="w-40"
        />
      </div>

      {/* Contextual bulk assign/remove strip — shown only while in that mode
          (launched from the Tags menu); its target follows the live selection. */}
      {tagMode && (
        <TagActionBar
          portfolioId={portfolioId}
          mode={tagMode}
          selectedIds={selectedIds}
          selectAllMatching={selectAllMatching}
          hasActiveFilters={hasActiveFilters}
          filterGroups={allFilterGroups}
          matchingCount={matchingCount}
          allLoadedSelected={allLoadedSelected}
          moreMatchThanLoaded={moreMatchThanLoaded}
          onSelectAllMatching={() => setSelectAllMatching(true)}
          onClearSelection={clearSelection}
          onClose={() => {
            setTagMode(null);
            clearSelection();
          }}
        />
      )}

      <BulkTagUploadModal
        portfolioId={portfolioId}
        open={tagUploadOpen}
        onOpenChange={setTagUploadOpen}
      />

      {/* Not-modelled nudge — turns a wall of empty cells into a next step.
          Mixed-safe: shown whenever any loaded property lacks a plan, so a
          part-modelled portfolio still gets the prompt for the rest. */}
      {showModelNudge && (
          <div className="mb-3 flex flex-wrap items-center gap-3 rounded-xl border border-brandmidblue/25 bg-brandlightblue/40 px-4 py-3">
            <div className="flex min-w-0 flex-1 items-start gap-3">
              <PlayIcon className="mt-0.5 h-5 w-5 shrink-0 text-brandmidblue" />
              <div className="min-w-0">
                <p className="text-sm font-semibold text-brandblue">
                  {unmodelledInView === totalCount
                    ? "None of these properties have been modelled yet"
                    : `${unmodelledInView.toLocaleString()} ${unmodelledInView === 1 ? "property hasn't" : "properties haven't"} been modelled yet`}
                </p>
                <p className="text-xs text-slate-600">
                  Run modelling to generate Expected EPC ratings and retrofit
                  costs for them.
                </p>
              </div>
            </div>
            <div className="flex shrink-0 items-center gap-1">
              <button
                onClick={() =>
                  router.push(`/portfolio/${portfolioId}/modelling/run`)
                }
                className="inline-flex items-center gap-1.5 rounded-lg bg-primary px-3 py-1.5 text-xs font-semibold text-white transition hover:opacity-90"
              >
                <PlayIcon className="h-3.5 w-3.5" />
                Run modelling
              </button>
              <button
                onClick={() => setModelNudgeDismissed(true)}
                aria-label="Dismiss"
                className="rounded-md p-1.5 text-slate-400 transition hover:text-brandblue"
              >
                <XMarkIcon className="h-4 w-4" />
              </button>
            </div>
          </div>
        )}

      {/* Display-limit notice */}
      {isAtDisplayLimit && hasMore && (
        <div className="mb-3 flex items-center gap-2 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-500">
          <span>
            Showing{" "}
            <span className="font-semibold text-primary">
              {tableData.length.toLocaleString()}
            </span>{" "}
            of{" "}
            <span className="font-semibold text-primary">
              {filteredTotal.toLocaleString()}
            </span>{" "}
            properties — more load automatically as you navigate to the last
            page.
          </span>
        </div>
      )}

      {/* Body: sidebar + table */}
      <div className="flex items-start">
        {/* Collapsible filter sidebar */}
        <div
          className={[
            "shrink-0 overflow-hidden transition-all duration-300 ease-in-out rounded-xl",
            sidebarOpen
              ? "w-72 opacity-100 border border-slate-100 bg-slate-50"
              : "w-0 opacity-0",
          ].join(" ")}
        >
          <div className="w-72">
            <p className="px-4 pt-4 pb-0 text-[9px] font-black text-primary uppercase tracking-widest">
              Curate Selection
            </p>
            <PropertyFilters
              portfolioId={portfolioId}
              filterGroups={filterGroups}
              onChange={setFilterGroups}
            />
          </div>
        </div>

        {/* Table area */}
        <div className="flex-1 min-w-0 bg-white rounded-xl border border-slate-100 shadow-sm relative">
          {((isFetching && !isLoading) || isFetchingMore) && <LoadingOverlay />}

          {isLoading ? (
            <div className="p-6 text-slate-400 text-sm">
              Loading properties…
            </div>
          ) : isError ? (
            <div className="p-6 text-red-500 text-sm">
              Failed to load properties.
            </div>
          ) : queryData.length === 0 && hasActiveFilters ? (
            <div className="p-10 text-center text-slate-500">
              <p className="text-sm">No properties match your filters.</p>
              <button
                onClick={clearAll}
                className="mt-3 text-xs text-primary underline"
              >
                Clear filters
              </button>
            </div>
          ) : queryData.length === 0 ? (
            <EmptyPropertyState />
          ) : (
            <DataTable
              data={tableData}
              columns={tagMode ? columns : columnsWithoutSelect}
              columnVisibility={columnVisibility}
              onColumnVisibilityChange={
                setColumnVisibility as (
                  updater: Updater<VisibilityState>,
                ) => void
              }
              pagination={pagination}
              onPaginationChange={
                setPagination as (updater: Updater<PaginationState>) => void
              }
              rowSelection={rowSelection}
              onRowSelectionChange={handleRowSelectionChange}
            />
          )}
        </div>
      </div>
    </div>
  );
}
