/**
 * Gitea read client. Config + injected fetch → typed, normalized domain
 * objects with scheduler facts precomputed. `getIssue` proved the vertical
 * slice (P1-1); `listIssues`/`listMilestones` are the reconcile reads (P1-4)
 * layered on the same `request` seam. Pagination is handled here so callers
 * get the full set.
 */

import { extractLabelFacts } from '../labels/label-schema.js'
import {
  GiteaApiError,
  type FetchLike,
  type GiteaConfig,
  type GiteaIssue,
  type GiteaMilestone,
  type GiteaMilestoneRef,
} from './types.js'

/** The subset of gitea's raw issue JSON we read. */
interface RawLabel {
  name: string
}
interface RawMilestone {
  id: number
  title: string
  due_on: string | null
}
interface RawUser {
  login: string
}
interface RawIssue {
  number: number
  title: string
  body: string | null
  state: string
  labels: RawLabel[] | null
  milestone: RawMilestone | null
  assignee: RawUser | null
  assignees: RawUser[] | null
  created_at: string
  updated_at: string
  closed_at: string | null
  html_url: string
  /** Present (non-null) when the row is actually a pull request. */
  pull_request?: unknown
}
interface RawMilestoneFull {
  id: number
  title: string
  description: string | null
  due_on: string | null
  state: string
  open_issues: number
  closed_issues: number
}

export interface ListIssuesOptions {
  /** @default 'all' */
  state?: 'open' | 'closed' | 'all'
}

export interface GiteaClient {
  /** Fetch one issue by its per-repo index, normalized. */
  getIssue(index: number): Promise<GiteaIssue>
  /** Fetch every issue (all pages), pull requests excluded. */
  listIssues(opts?: ListIssuesOptions): Promise<GiteaIssue[]>
  /** Fetch every milestone (all pages). */
  listMilestones(): Promise<GiteaMilestone[]>
  /** The issue indices this issue depends on (its blockers). */
  getIssueDependencies(index: number): Promise<number[]>
}

/** Map raw gitea issue JSON to the normalized domain shape. Pure. */
export function normalizeIssue(raw: RawIssue): GiteaIssue {
  const labels = (raw.labels ?? []).map((l) => l.name)
  const milestone: GiteaMilestoneRef | null = raw.milestone
    ? { id: raw.milestone.id, title: raw.milestone.title, dueOn: raw.milestone.due_on }
    : null
  const assignees = (raw.assignees ?? []).map((u) => u.login)

  return {
    number: raw.number,
    title: raw.title,
    body: raw.body ?? '',
    state: raw.state === 'closed' ? 'closed' : 'open',
    labels,
    facts: extractLabelFacts(labels),
    milestone,
    assignee: raw.assignee?.login ?? null,
    assignees,
    createdAt: raw.created_at,
    updatedAt: raw.updated_at,
    closedAt: raw.closed_at,
    url: raw.html_url,
  }
}

/** Map a raw milestone resource to the normalized shape. Pure. */
export function normalizeMilestone(raw: RawMilestoneFull): GiteaMilestone {
  return {
    id: raw.id,
    title: raw.title,
    description: raw.description ?? '',
    dueOn: raw.due_on,
    state: raw.state === 'closed' ? 'closed' : 'open',
    openIssues: raw.open_issues,
    closedIssues: raw.closed_issues,
  }
}

const PAGE_LIMIT = 50

export function createGiteaClient(config: GiteaConfig, fetchImpl: FetchLike): GiteaClient {
  const apiBase = `${config.baseUrl.replace(/\/+$/, '')}/api/v1`
  const repoBase = `${apiBase}/repos/${config.owner}/${config.repo}`

  async function request(path: string): Promise<unknown> {
    const res = await fetchImpl(`${repoBase}${path}`, {
      headers: {
        Authorization: `token ${config.token}`,
        Accept: 'application/json',
      },
    })
    if (!res.ok) {
      const body = await res.text().catch(() => '')
      throw new GiteaApiError(res.status, `GET ${path} failed (${res.status})`, body)
    }
    return res.json()
  }

  /** Follow gitea's page-limit pagination until a short page is returned. */
  async function requestAll<T>(build: (page: number) => string): Promise<T[]> {
    const out: T[] = []
    for (let page = 1; ; page++) {
      const batch = (await request(build(page))) as T[]
      out.push(...batch)
      if (batch.length < PAGE_LIMIT) return out
    }
  }

  return {
    async getIssue(index) {
      return normalizeIssue((await request(`/issues/${index}`)) as RawIssue)
    },

    async listIssues(opts) {
      const state = opts?.state ?? 'all'
      const raw = await requestAll<RawIssue>(
        (page) => `/issues?type=issues&state=${state}&page=${page}&limit=${PAGE_LIMIT}`,
      )
      // `type=issues` should exclude PRs, but guard anyway.
      return raw.filter((r) => r.pull_request == null).map(normalizeIssue)
    },

    async listMilestones() {
      const raw = await requestAll<RawMilestoneFull>(
        (page) => `/milestones?state=all&page=${page}&limit=${PAGE_LIMIT}`,
      )
      return raw.map(normalizeMilestone)
    },

    async getIssueDependencies(index) {
      const raw = (await request(`/issues/${index}/dependencies`)) as { number: number }[]
      return raw.map((d) => d.number)
    },
  }
}
