/**
 * `GitGraphView` — full git-graph layout (L8 / git-graph view).
 *
 * Renders the commit DAG for a project: each row shows an SVG node+edges on
 * the left, ref badges (branches in primary blue, tags in amber), a short
 * hash, the commit summary, author, and relative date. Clicking a row opens a
 * detail panel on the right.
 *
 * Data flow: `useGitGraph` → `computeGraphRows` → render rows.
 * Never calls `invoke()` — all data comes through the GitGateway port.
 */

import { useCallback, useEffect, useState } from "react";

import type { GraphCommit } from "@/domain";
import { useGateways } from "@/app/di";
import { Spinner, cn } from "@/shared";
import { computeGraphRows, type GraphLink, type GraphRow } from "./graphLayout";

// ---------------------------------------------------------------------------
// Hook
// ---------------------------------------------------------------------------

interface GitGraphViewModel {
  commits: GraphCommit[];
  rows: GraphRow[];
  busy: boolean;
  error: string | null;
  refresh: () => Promise<void>;
}

const GRAPH_LIMIT = 200;

function useGitGraph(projectId: string): GitGraphViewModel {
  const { git } = useGateways();
  const [commits, setCommits] = useState<GraphCommit[]>([]);
  const [rows, setRows] = useState<GraphRow[]>([]);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const refresh = useCallback(async () => {
    setBusy(true);
    setError(null);
    try {
      const data = await git.graph(projectId, GRAPH_LIMIT);
      setCommits(data);
      setRows(computeGraphRows(data));
    } catch (e) {
      setError(e && typeof e === "object" && "message" in e ? String((e as { message: unknown }).message) : String(e));
    } finally {
      setBusy(false);
    }
  }, [git, projectId]);

  useEffect(() => {
    void refresh();
  }, [refresh]);

  return { commits, rows, busy, error, refresh };
}

// ---------------------------------------------------------------------------
// SVG graph column
// ---------------------------------------------------------------------------

/** Width per lane in the SVG graph column. */
const LANE_W = 16;
/** Row height must match the rendered row height so lanes connect across rows. */
const ROW_H = 36;
/** Radius of the commit dot. */
const DOT_R = 4.5;

/** Horizontal centre of a lane. */
const laneX = (lane: number) => lane * LANE_W + LANE_W / 2;

/** SVG path for a link spanning `y0`→`y1` between two lanes (curved when bent). */
function linkPath(link: GraphLink, y0: number, y1: number): string {
  const x0 = laneX(link.fromLane);
  const x1 = laneX(link.toLane);
  if (x0 === x1) return `M ${x0} ${y0} L ${x1} ${y1}`; // straight lane line
  const ym = (y0 + y1) / 2;
  return `M ${x0} ${y0} C ${x0} ${ym} ${x1} ${ym} ${x1} ${y1}`; // smooth S-bend
}

interface GraphColumnProps {
  row: GraphRow;
  /** Total lane columns across the whole graph (fixed width so lanes align). */
  laneCount: number;
}

/** Renders the SVG lanes (incoming + outgoing) and the commit node for one row. */
function GraphColumn({ row, laneCount }: GraphColumnProps) {
  const w = laneCount * LANE_W + LANE_W / 2;
  const mid = ROW_H / 2;
  const cx = laneX(row.lane);

  return (
    <svg width={w} height={ROW_H} aria-hidden className="shrink-0 overflow-visible">
      {/* Top half: top boundary → node mid */}
      {row.incoming.map((link, i) => (
        <path
          key={`in-${i}`}
          d={linkPath(link, 0, mid)}
          fill="none"
          stroke={link.color}
          strokeWidth={2}
        />
      ))}
      {/* Bottom half: node mid → bottom boundary */}
      {row.outgoing.map((link, i) => (
        <path
          key={`out-${i}`}
          d={linkPath(link, mid, ROW_H)}
          fill="none"
          stroke={link.color}
          strokeWidth={2}
        />
      ))}
      {/* Commit node: filled dot with a dark ring so it reads over the lanes */}
      <circle cx={cx} cy={mid} r={DOT_R + 1.5} className="fill-canvas" />
      <circle
        cx={cx}
        cy={mid}
        r={DOT_R}
        fill={row.color}
        stroke="var(--color-canvas)"
        strokeWidth={1}
      />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// Relative date helper
// ---------------------------------------------------------------------------

function relativeDate(timestamp: number): string {
  const secs = Math.floor(Date.now() / 1000) - timestamp;
  if (secs < 60) return "just now";
  if (secs < 3600) return `${Math.floor(secs / 60)}m ago`;
  if (secs < 86400) return `${Math.floor(secs / 3600)}h ago`;
  if (secs < 2592000) return `${Math.floor(secs / 86400)}d ago`;
  if (secs < 31536000) return `${Math.floor(secs / 2592000)}mo ago`;
  return `${Math.floor(secs / 31536000)}y ago`;
}

// ---------------------------------------------------------------------------
// Detail panel
// ---------------------------------------------------------------------------

interface CommitDetailProps {
  commit: GraphCommit;
  onClose: () => void;
}

function CommitDetail({ commit, onClose }: CommitDetailProps) {
  const isTag = (ref: string) => ref.startsWith("tag:");

  return (
    <aside
      aria-label="commit detail"
      className="flex w-80 shrink-0 flex-col gap-3 overflow-y-auto border-l border-border bg-surface p-4"
    >
      <div className="flex items-start justify-between gap-2">
        <h3 className="text-sm font-semibold text-content">{commit.summary}</h3>
        <button
          type="button"
          aria-label="close commit detail"
          onClick={onClose}
          className="shrink-0 text-muted hover:text-content"
        >
          ×
        </button>
      </div>

      <dl className="flex flex-col gap-2 text-xs">
        <div>
          <dt className="font-medium text-faint">Hash</dt>
          <dd>
            <code className="text-content break-all">{commit.hash}</code>
          </dd>
        </div>
        <div>
          <dt className="font-medium text-faint">Author</dt>
          <dd className="text-content">{commit.author}</dd>
        </div>
        <div>
          <dt className="font-medium text-faint">Date</dt>
          <dd className="text-content">
            {new Date(commit.timestamp * 1000).toLocaleString()}
          </dd>
        </div>
        {commit.parents.length > 0 && (
          <div>
            <dt className="font-medium text-faint">Parents</dt>
            <dd className="flex flex-col gap-0.5">
              {commit.parents.map((p) => (
                <code key={p} className="text-content">
                  {p.slice(0, 8)}
                </code>
              ))}
            </dd>
          </div>
        )}
        {commit.refs.length > 0 && (
          <div>
            <dt className="font-medium text-faint">Refs</dt>
            <dd className="flex flex-wrap gap-1">
              {commit.refs.map((ref) => (
                <span
                  key={ref}
                  className={cn(
                    "rounded px-1 py-0.5 text-xs font-medium text-base",
                    isTag(ref) ? "bg-warning/20 text-warning" : "bg-primary/20 text-primary",
                  )}
                >
                  {ref}
                </span>
              ))}
            </dd>
          </div>
        )}
      </dl>
    </aside>
  );
}

// ---------------------------------------------------------------------------
// Main component
// ---------------------------------------------------------------------------

export interface GitGraphViewProps {
  projectId: string;
}

export function GitGraphView({ projectId }: GitGraphViewProps) {
  const vm = useGitGraph(projectId);
  const [selectedHash, setSelectedHash] = useState<string | null>(null);

  const selectedCommit =
    selectedHash != null
      ? vm.commits.find((c) => c.hash === selectedHash) ?? null
      : null;

  // Fixed lane-column count across all rows so the lanes line up vertically.
  const laneCount = vm.rows.reduce((m, r) => Math.max(m, r.laneCount), 1);

  const isTag = (ref: string) => ref.startsWith("tag:");

  return (
    <div className="flex flex-1 overflow-hidden" aria-label="git graph">
      {/* ── Graph + commit list ── */}
      <div className="flex flex-1 flex-col overflow-hidden">
        {/* Toolbar */}
        <div className="flex shrink-0 items-center gap-2 border-b border-border bg-surface px-3 py-2">
          <span className="text-sm font-semibold text-content">Git Graph</span>
          {vm.busy && <Spinner size={14} />}
          <button
            type="button"
            aria-label="refresh git graph"
            onClick={() => void vm.refresh()}
            disabled={vm.busy}
            className="ml-auto text-xs text-muted hover:text-content disabled:opacity-40"
          >
            Refresh
          </button>
        </div>

        {/* Error */}
        {vm.error && (
          <p
            role="alert"
            className="mx-3 mt-2 shrink-0 rounded border border-danger/40 bg-danger/10 px-3 py-2 text-xs text-danger"
          >
            {vm.error}
          </p>
        )}

        {/* Rows */}
        <div className="flex-1 overflow-y-auto" role="list" aria-label="commits">
          {vm.rows.length === 0 && !vm.busy && (
            <p className="p-4 text-sm text-muted">No commits found.</p>
          )}
          {vm.rows.map((row) => {
            const isSelected = row.commit.hash === selectedHash;
            return (
              <div
                key={row.commit.hash}
                role="listitem"
                aria-label={`commit ${row.commit.hash.slice(0, 8)}: ${row.commit.summary}`}
                onClick={() =>
                  setSelectedHash(isSelected ? null : row.commit.hash)
                }
                className={cn(
                  "flex cursor-pointer items-center gap-2 border-b border-border/50 px-2 hover:bg-raised",
                  isSelected && "bg-raised",
                )}
                style={{ height: ROW_H }}
              >
                {/* SVG lanes + node */}
                <GraphColumn row={row} laneCount={laneCount} />

                {/* Ref badges */}
                {row.commit.refs.length > 0 && (
                  <div className="flex shrink-0 flex-wrap gap-1">
                    {row.commit.refs.map((ref) => (
                      <span
                        key={ref}
                        className={cn(
                          "rounded px-1 py-0.5 text-xs font-medium",
                          isTag(ref)
                            ? "bg-warning/20 text-warning"
                            : "bg-primary/20 text-primary",
                        )}
                      >
                        {ref}
                      </span>
                    ))}
                  </div>
                )}

                {/* Short hash */}
                <code className="shrink-0 text-xs text-faint">
                  {row.commit.hash.slice(0, 8)}
                </code>

                {/* Summary */}
                <span className="min-w-0 flex-1 truncate text-sm text-content">
                  {row.commit.summary}
                </span>

                {/* Author + relative date */}
                <span className="shrink-0 text-xs text-muted">
                  {row.commit.author}
                </span>
                <span className="shrink-0 text-xs text-faint">
                  {relativeDate(row.commit.timestamp)}
                </span>
              </div>
            );
          })}
        </div>
      </div>

      {/* ── Detail panel (shown when a commit is selected) ── */}
      {selectedCommit && (
        <CommitDetail
          commit={selectedCommit}
          onClose={() => setSelectedHash(null)}
        />
      )}
    </div>
  );
}
