// [nexus] Phase 11 — Projects list page (hero-stat cards).
//
// Replaces the old Paperclip EntityRow table with the spec §7.1
// hero-stat card grid. Cards stack full-width on <1024px and 2-up on
// >=1024px. A forest-green "+ NEW PROJECT" CTA sits top-right. When
// the project list is empty we render the 96px "NO PROJECTS YET"
// empty state canvas.
//
// Phase 11.5 decision (wave 2.5 controller pass): the shared Project
// type is missing milestoneProgress / nextGate / costBurned / agent
// counts, but we don't want em-dash city to kill the visual impact of
// the 72px volt hero percentage. Instead we compute best-effort
// derivatives from data that DOES exist:
//   - progress  ← closed_issue_count / total_issue_count (from one
//                 shared issuesApi.list query, grouped per-project)
//   - nextGateName ← first pending approval whose payload.projectId
//                 matches (from one shared approvalsApi.list query)
//   - lastActivity ← max(project.updatedAt, newest issue activity in
//                 the project) rendered as "8m ago"-style diff
// Each proxy is marked with a `// TODO(phase-11.5)` comment and will
// be replaced by a real backend field when it lands.
import { useEffect, useMemo } from "react";
import { VOCAB } from "@paperclipai/branding";
import { useQuery } from "@tanstack/react-query";
import { useNavigate } from "@/lib/router";
import type { Issue, Approval } from "@paperclipai/shared";
import { projectsApi } from "../api/projects";
import { issuesApi } from "../api/issues";
import { approvalsApi } from "../api/approvals";
import { useCompany } from "../context/CompanyContext";
import { useDialog } from "../context/DialogContext";
import { useBreadcrumbs } from "../context/BreadcrumbContext";
import { queryKeys } from "../lib/queryKeys";
import { PageSkeleton } from "../components/PageSkeleton";
import { EmptyState } from "../components/EmptyState";
import { Hexagon } from "lucide-react";
import { projectUrl } from "../lib/utils";
import { cn } from "@/lib/utils";
import { ProjectCard, type ProjectCardStatus } from "../components/projects/ProjectCard";

/** Issue statuses that count as "closed" for progress computation. */
const CLOSED_ISSUE_STATUSES = new Set<string>(["done", "cancelled"]);

/**
 * Derive per-project aggregates from a flat list of issues and pending
 * approvals. Runs once on the client per Projects page render — cheap
 * enough for the ~dozens-of-projects scale the list targets.
 *
 * TODO(phase-11.5): replace with server-side aggregates on the Project
 * record when the backend exposes them.
 */
function buildProjectDerivatives(
  issues: Issue[] | undefined,
  pendingApprovals: Approval[] | undefined,
): Map<
  string,
  {
    progress: number | null;
    closedCount: number;
    totalCount: number;
    nextGateName: string | null;
    latestActivityAt: Date | null;
  }
> {
  const map = new Map<
    string,
    {
      progress: number | null;
      closedCount: number;
      totalCount: number;
      nextGateName: string | null;
      latestActivityAt: Date | null;
    }
  >();

  for (const issue of issues ?? []) {
    if (!issue.projectId) continue;
    const row = map.get(issue.projectId) ?? {
      progress: null,
      closedCount: 0,
      totalCount: 0,
      nextGateName: null,
      latestActivityAt: null,
    };
    row.totalCount += 1;
    if (CLOSED_ISSUE_STATUSES.has(issue.status)) {
      row.closedCount += 1;
    }
    // Track the newest issue update timestamp for lastActivity.
    const issueUpdated = issue.updatedAt
      ? typeof issue.updatedAt === "string"
        ? new Date(issue.updatedAt)
        : issue.updatedAt
      : null;
    if (
      issueUpdated &&
      (!row.latestActivityAt || issueUpdated.getTime() > row.latestActivityAt.getTime())
    ) {
      row.latestActivityAt = issueUpdated;
    }
    map.set(issue.projectId, row);
  }

  for (const [projectId, row] of map.entries()) {
    row.progress = row.totalCount > 0 ? (row.closedCount / row.totalCount) * 100 : null;
    map.set(projectId, row);
  }

  for (const approval of pendingApprovals ?? []) {
    // Approval.payload is untyped Record<string, unknown>; probe defensively.
    const payload = approval.payload as Record<string, unknown> | undefined;
    const projectId = typeof payload?.projectId === "string" ? payload.projectId : null;
    if (!projectId) continue;
    const row = map.get(projectId) ?? {
      progress: null,
      closedCount: 0,
      totalCount: 0,
      nextGateName: null,
      latestActivityAt: null,
    };
    if (row.nextGateName === null) {
      const title =
        (typeof payload?.title === "string" && payload.title) ||
        (typeof payload?.name === "string" && payload.name) ||
        approval.type;
      row.nextGateName = title;
    }
    map.set(projectId, row);
  }

  return map;
}

function formatRelative(then: Date | null): string | null {
  if (!then) return null;
  const ms = Date.now() - then.getTime();
  const mins = Math.floor(ms / 60_000);
  if (mins < 1) return "just now";
  if (mins < 60) return `${mins}m ago`;
  const hrs = Math.floor(mins / 60);
  if (hrs < 24) return `${hrs}h ago`;
  const days = Math.floor(hrs / 24);
  return `${days}d ago`;
}

function deriveStatus(status: string): ProjectCardStatus {
  // Waiting > working > idle priority. For Phase 11 we only have the
  // coarse ProjectStatus enum to work with; the finer-grained "has
  // pending gate" / "has working agent" checks are data gaps.
  if (status === "paused" || status === "awaiting_input") return "waiting";
  if (status === "active" || status === "running") return "working";
  return "idle";
}

export function Projects() {
  const { selectedCompanyId } = useCompany();
  const { openNewProject } = useDialog();
  const { setBreadcrumbs } = useBreadcrumbs();
  const navigate = useNavigate();

  useEffect(() => {
    setBreadcrumbs([{ label: "Projects" }]);
  }, [setBreadcrumbs]);

  const { data: allProjects, isLoading, error } = useQuery({
    queryKey: queryKeys.projects.list(selectedCompanyId!),
    queryFn: () => projectsApi.list(selectedCompanyId!),
    enabled: !!selectedCompanyId,
  });

  // TODO(phase-11.5): replace with server-side aggregates on Project record.
  // For now we fetch all issues and all pending approvals once and derive
  // per-project counts on the client. This is O(#issues + #approvals) on the
  // frontend but only fires per-company, not per-project, so it stays cheap.
  const { data: allIssues } = useQuery({
    queryKey: queryKeys.issues.list(selectedCompanyId!),
    queryFn: () => issuesApi.list(selectedCompanyId!),
    enabled: !!selectedCompanyId,
  });
  const { data: pendingApprovals } = useQuery({
    queryKey: [...queryKeys.approvals.list(selectedCompanyId!), "pending"],
    queryFn: () => approvalsApi.list(selectedCompanyId!, "pending"),
    enabled: !!selectedCompanyId,
  });

  const projects = useMemo(
    () => (allProjects ?? []).filter((p) => !p.archivedAt),
    [allProjects],
  );

  const derivatives = useMemo(
    () => buildProjectDerivatives(allIssues, pendingApprovals),
    [allIssues, pendingApprovals],
  );

  if (!selectedCompanyId) {
    return (
      <EmptyState
        icon={Hexagon}
        message={`Select a ${VOCAB.company.toLowerCase()} to view projects.`}
      />
    );
  }

  if (isLoading) {
    return <PageSkeleton variant="list" />;
  }

  // Empty state — full-canvas 96px volt hero.
  if (projects.length === 0) {
    return (
      <div
        data-testid="projects-empty-state"
        className="flex min-h-[60vh] flex-col items-start justify-center gap-8"
      >
        <h2
          data-testid="projects-empty-hero"
          className="font-black uppercase text-[96px] leading-none text-primary"
        >
          No projects yet
        </h2>
        <button
          type="button"
          onClick={openNewProject}
          data-testid="projects-empty-cta"
          className={cn(
            "inline-flex items-center gap-2 rounded-md px-5 py-3",
            "bg-[#166534] text-white",
            "text-[14px] font-semibold uppercase tracking-[0.1em]",
            "hover:bg-[#166534]/90 transition-colors",
            "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background",
          )}
        >
          <span aria-hidden="true">⊕</span> Start your first project
        </button>
      </div>
    );
  }

  return (
    <div data-testid="projects-page" className="space-y-6">
      {/* Title row: section heading + new project CTA */}
      <div className="flex items-center justify-between">
        <h2 className="text-[14px] font-bold uppercase tracking-[0.14em] text-muted-foreground">
          Projects
        </h2>
        <button
          type="button"
          onClick={openNewProject}
          data-testid="projects-new-project-cta"
          className={cn(
            "inline-flex items-center gap-2 rounded-md px-4 py-2",
            "bg-[#166534] text-white",
            "text-[12px] font-semibold uppercase tracking-[0.12em]",
            "hover:bg-[#166534]/90 transition-colors",
            "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background",
          )}
        >
          <span aria-hidden="true">⊕</span> New project
        </button>
      </div>

      {error && <p className="text-sm text-destructive">{error.message}</p>}

      {/* Card grid: 1 col <1024px, 2 cols >=1024px, 16px gap */}
      <div
        data-testid="projects-grid"
        className="grid grid-cols-1 gap-4 lg:grid-cols-2"
      >
        {projects.map((project) => {
          const slug = (project.urlKey ?? project.name).toUpperCase();
          const derived = derivatives.get(project.id);

          // Take the newer of project.updatedAt and the latest issue
          // update inside this project — whichever happened more recently.
          const projectUpdatedAt = project.updatedAt
            ? typeof project.updatedAt === "string"
              ? new Date(project.updatedAt)
              : project.updatedAt
            : null;
          const latestActivity =
            derived?.latestActivityAt &&
            (!projectUpdatedAt || derived.latestActivityAt.getTime() > projectUpdatedAt.getTime())
              ? derived.latestActivityAt
              : projectUpdatedAt;

          return (
            <ProjectCard
              key={project.id}
              slug={slug}
              status={deriveStatus(project.status)}
              // TODO(phase-11.5): replace with real milestoneProgress from
              // the Project record. For now progress is a proxy:
              // closed_issues / total_issues × 100.
              progress={derived?.progress ?? null}
              // Phase / milestone data is a hard gap (no milestones on
              // the Project record). Leave null — ProjectCard renders "—".
              phase={null}
              // TODO(phase-11.5): replace with the real next-gate slot on
              // the Project record. Currently derived from the first
              // pending approval whose payload.projectId matches.
              nextGateName={derived?.nextGateName ?? null}
              // costBurnedCents is still a hard gap in Wave 2 — deferred
              // to phase 11.5.
              costBurnedCents={null}
              lastActivity={formatRelative(latestActivity)}
              ariaLabel={`Open project ${slug}`}
              onClick={() => navigate(projectUrl(project))}
            />
          );
        })}
      </div>
    </div>
  );
}
