/**
 * GitHub REST API client.
 *
 * Uses fetch + the `X-GitHub-Api-Version` header that the current REST
 * API documents. Authentication is optional but recommended — anonymous
 * requests are limited to 60/h per IP, while an authenticated user gets
 * 5,000/h. We read the token from `GITHUB_TOKEN`, then `GH_TOKEN`,
 * then fall back to anonymous.
 */

import type {
  RepoStats,
  IssueSummary,
  PullSummary,
  ReleaseSummary,
  ContributorSummary,
  ParsedRepo,
} from './types.js';

export interface FetchOptions {
  token?: string;
  /** Maximum number of issues to fetch. Default: 30. */
  issueLimit?: number;
  /** Maximum number of PRs to fetch. Default: 20. */
  pullLimit?: number;
  /** Maximum number of releases to fetch. Default: 10. */
  releaseLimit?: number;
  /** Maximum number of contributors to fetch. Default: 20. */
  contributorLimit?: number;
  /** When true, only open issues are returned. Default: false. */
  openIssuesOnly?: boolean;
  /** When true, only open PRs are returned. Default: false. */
  openPullsOnly?: boolean;
  /** Custom fetch implementation (used for testing). */
  fetchImpl?: typeof fetch;
}

const DEFAULT_USER_AGENT = 'repo-stats/0.1.0 (https://github.com/ignaciolagosruiz/repo-stats)';
const API_ROOT = 'https://api.github.com';

function readToken(explicit?: string): string | undefined {
  if (explicit) return explicit;
  return process.env.GITHUB_TOKEN || process.env.GH_TOKEN || undefined;
}

interface RequestOptions {
  token?: string;
  fetchImpl: typeof fetch;
  query?: Record<string, string | number | boolean | undefined>;
}

async function gh<T>(path: string, opts: RequestOptions): Promise<T> {
  const url = new URL(API_ROOT + path);
  if (opts.query) {
    for (const [k, v] of Object.entries(opts.query)) {
      if (v !== undefined) url.searchParams.set(k, String(v));
    }
  }
  const headers: Record<string, string> = {
    Accept: 'application/vnd.github+json',
    'X-GitHub-Api-Version': '2022-11-28',
    'User-Agent': DEFAULT_USER_AGENT,
  };
  if (opts.token) {
    headers.Authorization = `Bearer ${opts.token}`;
  }
  const res = await opts.fetchImpl(url.toString(), { headers });
  if (!res.ok) {
    let detail = '';
    try {
      const body = await res.json() as { message?: string };
      detail = body.message ? `: ${body.message}` : '';
    } catch {
      // ignore body parse error
    }
    throw new Error(`GitHub API ${res.status} ${res.statusText}${detail} (GET ${path})`);
  }
  return (await res.json()) as T;
}

interface RawRepo {
  full_name: string;
  description: string | null;
  language: string | null;
  license: { spdx_id: string | null } | null;
  stargazers_count: number;
  forks_count: number;
  subscribers_count: number;
  open_issues_count: number;
  default_branch: string;
  topics: string[];
  created_at: string;
  updated_at: string;
  pushed_at: string;
  size: number;
  has_wiki: boolean;
  has_pages: boolean;
  archived: boolean;
  homepage: string | null;
}

interface RawIssue {
  number: number;
  title: string;
  state: 'open' | 'closed';
  user: { login: string } | null;
  created_at: string;
  updated_at: string;
  comments: number;
  labels: Array<{ name: string }>;
  pull_request?: unknown;
}

interface RawPull {
  number: number;
  title: string;
  state: 'open' | 'closed';
  user: { login: string } | null;
  created_at: string;
  updated_at: string;
  merged_at: string | null;
  draft: boolean;
  review_comments: number;
  additions: number;
  deletions: number;
  changed_files: number;
}

interface RawRelease {
  tag_name: string;
  name: string | null;
  published_at: string | null;
  draft: boolean;
  prerelease: boolean;
  author: { login: string } | null;
}

interface RawContributor {
  login: string;
  contributions: number;
}

function repoPath(repo: ParsedRepo): string {
  return `/repos/${repo.owner}/${repo.name}`;
}

function repoStatsFromRaw(raw: RawRepo, repo: ParsedRepo): RepoStats {
  return {
    repo,
    description: raw.description,
    primaryLanguage: raw.language,
    license: raw.license?.spdx_id ?? null,
    stars: raw.stargazers_count,
    forks: raw.forks_count,
    watchers: raw.subscribers_count,
    openIssues: raw.open_issues_count,
    defaultBranch: raw.default_branch,
    topics: raw.topics,
    createdAt: raw.created_at,
    updatedAt: raw.updated_at,
    pushedAt: raw.pushed_at,
    sizeKb: raw.size,
    hasWiki: raw.has_wiki,
    hasPages: raw.has_pages,
    archived: raw.archived,
    homepage: raw.homepage,
  };
}

function issueFromRaw(raw: RawIssue): IssueSummary {
  return {
    number: raw.number,
    title: raw.title,
    state: raw.state,
    user: raw.user?.login ?? '(deleted)',
    createdAt: raw.created_at,
    updatedAt: raw.updated_at,
    comments: raw.comments,
    labels: raw.labels.map((l) => l.name),
    pullRequest: Boolean(raw.pull_request),
  };
}

function pullFromRaw(raw: RawPull): PullSummary {
  let state: PullSummary['state'];
  if (raw.merged_at) state = 'merged';
  else state = raw.state;
  return {
    number: raw.number,
    title: raw.title,
    state,
    user: raw.user?.login ?? '(deleted)',
    createdAt: raw.created_at,
    updatedAt: raw.updated_at,
    mergedAt: raw.merged_at,
    draft: raw.draft,
    reviewComments: raw.review_comments,
    additions: raw.additions,
    deletions: raw.deletions,
    changedFiles: raw.changed_files,
  };
}

function releaseFromRaw(raw: RawRelease): ReleaseSummary {
  return {
    tagName: raw.tag_name,
    name: raw.name,
    publishedAt: raw.published_at,
    draft: raw.draft,
    prerelease: raw.prerelease,
    author: raw.author?.login ?? '(deleted)',
  };
}

function contributorFromRaw(raw: RawContributor): ContributorSummary {
  return { login: raw.login, contributions: raw.contributions };
}

export interface FetchedStats {
  repo: RepoStats;
  issues: IssueSummary[];
  pulls: PullSummary[];
  releases: ReleaseSummary[];
  contributors: ContributorSummary[];
}

export async function fetchRepoStats(
  repo: ParsedRepo,
  options: FetchOptions = {},
): Promise<FetchedStats> {
  const token = readToken(options.token);
  const fetchImpl = options.fetchImpl ?? globalThis.fetch;
  if (!fetchImpl) {
    throw new Error('No fetch implementation available; pass options.fetchImpl or upgrade Node.');
  }
  const reqOpts: RequestOptions = { token, fetchImpl };

  const repoRaw = await gh<RawRepo>(repoPath(repo), reqOpts);
  const stats = repoStatsFromRaw(repoRaw, repo);

  const issueState = options.openIssuesOnly ? 'open' : 'all';
  const pullState = options.openPullsOnly ? 'open' : 'all';

  const [issuesRaw, pullsRaw, releasesRaw, contributorsRaw] = await Promise.all([
    gh<RawIssue[]>(`${repoPath(repo)}/issues`, {
      ...reqOpts,
      query: { state: issueState, per_page: options.issueLimit ?? 30, sort: 'updated', direction: 'desc' },
    }),
    gh<RawPull[]>(`${repoPath(repo)}/pulls`, {
      ...reqOpts,
      query: { state: pullState, per_page: options.pullLimit ?? 20, sort: 'updated', direction: 'desc' },
    }),
    gh<RawRelease[]>(`${repoPath(repo)}/releases`, {
      ...reqOpts,
      query: { per_page: options.releaseLimit ?? 10 },
    }),
    gh<RawContributor[]>(`${repoPath(repo)}/contributors`, {
      ...reqOpts,
      query: { per_page: options.contributorLimit ?? 20, anon: 'false' },
    }),
  ]);

  // The /issues endpoint returns both issues and PRs; drop the PRs since
  // we already fetch them from /pulls.
  const issues = issuesRaw.filter((i) => !i.pull_request).map(issueFromRaw);
  const pulls = pullsRaw.map(pullFromRaw);
  const releases = releasesRaw.map(releaseFromRaw);
  const contributors = contributorsRaw
    .map(contributorFromRaw)
    .sort((a, b) => b.contributions - a.contributions);

  return { repo: stats, issues, pulls, releases, contributors };
}
