<script setup lang="ts">
/**
 * Props-driven pull-request list composition. Renders a toolbar with
 * status-filter chips + a search input, then a list of PR rows. The
 * composition makes no HTTP calls — the consumer supplies a single
 * Promise-returning `onLoadPullRequests` callback that receives the
 * current filter object.
 *
 *   <PullRequestsList
 *     :on-load-pull-requests="(f) => fetch(`/api/prs?...`).then(r => r.json())"
 *     v-model:selected-id="selectedPr"
 *     @select="(pr) => router.push(pr.url)"
 *   />
 */
import { computed, onMounted, ref, watch } from "vue"
import {
  ArrowRight,
  GitMerge,
  GitPullRequest,
  GitPullRequestDraft,
  Loader2,
  Search,
  XCircle,
} from "@lucide/vue"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Skeleton } from "@/components/ui/skeleton"
import { cn } from "@/lib/utils"

export type PullRequestStatus =
  | "open"
  | "closed"
  | "merged"
  | "draft"

export interface PullRequest {
  id: string
  number?: number
  title: string
  authorName?: string
  authorAvatarUrl?: string
  status: PullRequestStatus
  createdAt?: string | Date
  updatedAt?: string | Date
  branchName?: string
  targetBranch?: string
  url?: string
}

export interface PullRequestsListFilter {
  status?: PullRequestStatus
  query?: string
}

export interface PullRequestsListProps {
  /** REQUIRED loader, invoked on mount and whenever the filter changes. */
  onLoadPullRequests: (
    filter?: PullRequestsListFilter,
  ) => Promise<PullRequest[]>
  /** Initial filter applied on mount. */
  defaultFilter?: PullRequestsListFilter
  /** Currently selected PR id (v-modelable as `selectedId`). */
  selectedId?: string
  /** Fired when a PR row is clicked. */
  onSelect?: (pr: PullRequest) => void
  label?: string
  name?: string | null
  helpText?: string
  errors?: string[]
  classList?: string[] | string
}

const props = withDefaults(defineProps<PullRequestsListProps>(), {
  errors: () => [],
  label: "Pull Requests",
})

const emit = defineEmits<{
  "update:selectedId": [value: string]
}>()

const status = ref<PullRequestStatus | "all">(
  props.defaultFilter?.status ?? "all",
)
const query = ref<string>(props.defaultFilter?.query ?? "")

const items = ref<PullRequest[]>([])
const loading = ref<boolean>(false)
const error = ref<string | null>(null)
let token = 0

const STATUS_CHIPS: { value: PullRequestStatus | "all"; label: string }[] = [
  { value: "all", label: "All" },
  { value: "open", label: "Open" },
  { value: "draft", label: "Draft" },
  { value: "merged", label: "Merged" },
  { value: "closed", label: "Closed" },
]

function statusVariant(s: PullRequestStatus): string {
  switch (s) {
    case "open":
      return "success"
    case "merged":
      return "info"
    case "closed":
      return "destructive"
    case "draft":
    default:
      return "secondary"
  }
}

function statusIcon(s: PullRequestStatus) {
  if (s === "merged") {
    return GitMerge
  }
  if (s === "closed") {
    return XCircle
  }
  if (s === "draft") {
    return GitPullRequestDraft
  }
  return GitPullRequest
}

function relativeTime(value: string | Date | undefined): string {
  if (!value) {
    return ""
  }
  const d = value instanceof Date ? value : new Date(value)
  const diff = Date.now() - d.getTime()
  if (Number.isNaN(diff)) {
    return ""
  }
  const sec = Math.round(diff / 1000)
  if (sec < 60) {
    return `${sec}s ago`
  }
  const min = Math.round(sec / 60)
  if (min < 60) {
    return `${min}m ago`
  }
  const hr = Math.round(min / 60)
  if (hr < 24) {
    return `${hr}h ago`
  }
  const day = Math.round(hr / 24)
  if (day < 30) {
    return `${day}d ago`
  }
  const month = Math.round(day / 30)
  if (month < 12) {
    return `${month}mo ago`
  }
  const year = Math.round(month / 12)
  return `${year}y ago`
}

function currentFilter(): PullRequestsListFilter {
  const f: PullRequestsListFilter = {}
  if (status.value !== "all") {
    f.status = status.value
  }
  if (query.value) {
    f.query = query.value
  }
  return f
}

async function reload(): Promise<void> {
  const myToken = ++token
  loading.value = true
  error.value = null
  try {
    const next = await props.onLoadPullRequests(currentFilter())
    if (myToken !== token) {
      return
    }
    items.value = Array.isArray(next) ? next : []
  } catch (err) {
    if (myToken !== token) {
      return
    }
    error.value = err instanceof Error ? err.message : String(err)
    items.value = []
  } finally {
    if (myToken === token) {
      loading.value = false
    }
  }
}

let debounceTimer: ReturnType<typeof setTimeout> | null = null
function scheduleReload(): void {
  if (debounceTimer !== null) {
    clearTimeout(debounceTimer)
  }
  debounceTimer = setTimeout(() => {
    debounceTimer = null
    void reload()
  }, 200)
}

function setStatus(next: PullRequestStatus | "all"): void {
  if (status.value === next) {
    return
  }
  status.value = next
  void reload()
}

function onQueryInput(e: Event): void {
  const next = (e.target as HTMLInputElement).value
  query.value = next
  scheduleReload()
}

function selectRow(pr: PullRequest): void {
  emit("update:selectedId", pr.id)
  if (props.onSelect) {
    props.onSelect(pr)
  }
}

function authorFallback(name?: string): string {
  if (!name) {
    return "?"
  }
  return name
    .split(/\s+/)
    .filter(Boolean)
    .slice(0, 2)
    .map((s) => s[0]?.toUpperCase() ?? "")
    .join("")
}

const selectedIdRef = computed<string | undefined>(() => props.selectedId)

onMounted(() => {
  void reload()
})

watch(
  () => props.defaultFilter,
  (next) => {
    if (!next) {
      return
    }
    status.value = next.status ?? "all"
    query.value = next.query ?? ""
    void reload()
  },
  { deep: true },
)

defineExpose({ reload })
</script>

<template>
  <Card
    :class="cn(props.classList)"
    :data-element-name="props.name ?? undefined"
    data-testid="pull-requests-list"
  >
    <CardHeader class="flex flex-col gap-3 pb-3">
      <CardTitle class="flex items-center gap-2 text-base">
        <GitPullRequest class="size-5 text-muted-foreground" />
        <span>{{ props.label }}</span>
      </CardTitle>
      <div class="flex flex-wrap items-center gap-2">
        <div class="flex flex-wrap items-center gap-1">
          <button
            v-for="chip in STATUS_CHIPS"
            :key="chip.value"
            type="button"
            :class="
              cn(
                'rounded-full border px-3 py-1 text-xs font-medium transition-colors',
                status === chip.value
                  ? 'border-primary bg-primary text-primary-foreground'
                  : 'border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground',
              )
            "
            :data-testid="`pull-requests-list-chip-${chip.value}`"
            :aria-pressed="status === chip.value"
            @click="setStatus(chip.value)"
          >
            {{ chip.label }}
          </button>
        </div>
        <div class="relative flex-1 min-w-48">
          <Search
            class="absolute left-2 top-1/2 -translate-y-1/2 size-4 text-muted-foreground"
          />
          <Input
            :model-value="query"
            placeholder="Search…"
            class="pl-8"
            data-testid="pull-requests-list-search"
            @input="onQueryInput"
          />
        </div>
      </div>
      <p
        v-if="props.helpText"
        class="text-sm text-muted-foreground"
        data-testid="pull-requests-list-help"
      >
        {{ props.helpText }}
      </p>
    </CardHeader>
    <CardContent class="flex flex-col gap-1 p-2">
      <div
        v-if="loading"
        class="flex flex-col gap-2 px-2 py-2"
        data-testid="pull-requests-list-loading"
      >
        <Skeleton class="h-12 w-full" />
        <Skeleton class="h-12 w-full" />
        <Skeleton class="h-12 w-full" />
      </div>
      <p
        v-else-if="error"
        class="px-2 py-2 text-sm text-destructive"
        data-testid="pull-requests-list-error"
        role="alert"
      >
        {{ error }}
      </p>
      <p
        v-else-if="items.length === 0"
        class="px-2 py-4 text-center text-sm text-muted-foreground"
        data-testid="pull-requests-list-empty"
      >
        No pull requests.
      </p>
      <ul
        v-else
        class="flex flex-col"
        role="list"
        data-testid="pull-requests-list-rows"
      >
        <li
          v-for="pr in items"
          :key="pr.id"
          :class="
            cn(
              'flex cursor-pointer items-center gap-3 rounded-md border border-transparent px-2 py-2 hover:bg-accent',
              selectedIdRef === pr.id
                ? 'border-primary bg-accent'
                : '',
            )
          "
          :data-testid="`pull-requests-list-row-${pr.id}`"
          :aria-selected="selectedIdRef === pr.id"
          @click="selectRow(pr)"
        >
          <component
            :is="statusIcon(pr.status)"
            class="size-4 shrink-0 text-muted-foreground"
          />
          <div class="flex min-w-0 flex-1 flex-col gap-0.5">
            <div class="flex items-center gap-2">
              <span class="truncate text-sm font-medium">{{ pr.title }}</span>
              <span
                v-if="pr.number !== undefined"
                class="text-xs text-muted-foreground"
              >
                #{{ pr.number }}
              </span>
              <Badge
                :variant="(statusVariant(pr.status) as 'success' | 'info' | 'destructive' | 'secondary')"
                size="sm"
                class="capitalize"
              >
                {{ pr.status }}
              </Badge>
            </div>
            <div
              class="flex flex-wrap items-center gap-2 text-xs text-muted-foreground"
            >
              <span
                v-if="pr.authorName"
                class="flex items-center gap-1.5"
              >
                <Avatar size="sm" class="size-5 text-[10px]">
                  <AvatarImage
                    v-if="pr.authorAvatarUrl"
                    :src="pr.authorAvatarUrl"
                    :alt="pr.authorName"
                  />
                  <AvatarFallback>{{ authorFallback(pr.authorName) }}</AvatarFallback>
                </Avatar>
                <span>{{ pr.authorName }}</span>
              </span>
              <span
                v-if="pr.branchName || pr.targetBranch"
                class="flex items-center gap-1"
              >
                <span v-if="pr.branchName" class="font-mono">{{ pr.branchName }}</span>
                <ArrowRight v-if="pr.branchName && pr.targetBranch" class="size-3" />
                <span v-if="pr.targetBranch" class="font-mono">{{ pr.targetBranch }}</span>
              </span>
              <span v-if="pr.updatedAt || pr.createdAt">
                {{ relativeTime(pr.updatedAt ?? pr.createdAt) }}
              </span>
            </div>
          </div>
          <Loader2
            v-if="false"
            class="size-3 animate-spin text-muted-foreground"
          />
        </li>
      </ul>
      <ul
        v-if="props.errors.length"
        class="px-2 pt-2 text-sm text-destructive"
        data-testid="pull-requests-list-errors"
        role="alert"
      >
        <li v-for="(err, i) in props.errors" :key="i">{{ err }}</li>
      </ul>
    </CardContent>
  </Card>
</template>
