/**
 * GitHub discovery — find Pi Coding Agent repositories via GitHub Search API.
 *
 * Searches by name, description, and topics using the canonical SEARCH_TERMS
 * from core, formatted as GitHub query syntax.
 *
 * Uses GitHub Search API with numbered-page pagination (max 100/page, 1000 results max).
 *
 * Requires `GITHUB_TOKEN` env var for reasonable rate limits:
 *   - Unauthenticated: 10 req/min (search), 60 req/hr (general)
 *   - Authenticated:   30 req/min (search), 5000 req/hr (general)
 */

import type { Cache } from "../core/cache.ts";
import { paginate } from "../core/paginate.ts";
import { SEARCH_TERMS } from "../core/terms.ts";
import { ThrottledFetcher } from "../core/throttle.ts";
import { type CategorizedEntry, EntrySource } from "../core/types.ts";
import { writeRaw } from "../discover/runner.ts";
import type { DiscoveryWriter } from "../discover/writer.ts";
import { formatKNumber } from "./scoring.ts";
import type { Source } from "./source.ts";

// ─── Config ────────────────────────────────────────────────────────────────────

const GITHUB_API = "https://api.github.com";
const PAGE_SIZE = 100;
/** GitHub Search API: 30 req/min authenticated, 10 req/min unauthenticated. */
const REQUESTS_PER_SECOND = 0.5;
/** GitHub Search returns at most 1000 results per query. */
const MAX_RESULTS = 1000;

/** Default queries: canonical search terms formatted for GitHub search. */
const DEFAULT_REPO_QUERIES = SEARCH_TERMS.map((term) => `${term} language:TypeScript`);

// ─── Types ─────────────────────────────────────────────────────────────────────

interface GitHubRepo {
	full_name: string;
	html_url: string;
	description: string | null;
	stargazers_count: number;
	forks_count: number;
	open_issues_count: number;
	topics: string[];
	language: string | null;
	archived: boolean;
	created_at: string;
	pushed_at: string | null;
	updated_at: string;
	size: number;
	license: { key: string; name: string; spdx_id: string } | null;
}

interface GitHubSearchResponse {
	total_count: number;
	incomplete_results: boolean;
	items: GitHubRepo[];
}

export interface GitHubSourceOptions {
	/** Override default repo search queries. */
	repoQueries?: string[] | undefined;
	/** Run in offline mode — only use cached responses, never hit the network. */
	offline?: boolean | undefined;
}

// ─── Helpers ───────────────────────────────────────────────────────────────────

/** Build a candidate from a GitHub repo. */
function repoToCandidate(
	repo: GitHubRepo,
	queryType: string,
	term: string,
	extra?: Record<string, unknown>,
) {
	return {
		url: repo.html_url,
		hint: `github-${queryType}:${term}`,
		id: repo.full_name.replace("/", "-"),
		metadata: {
			github_url: repo.html_url,
			repo_full_name: repo.full_name,
			description: repo.description ?? "",
			stars: repo.stargazers_count,
			forks: repo.forks_count,
			open_issues: repo.open_issues_count,
			topics: repo.topics ?? [],
			language: repo.language ?? null,
			archived: repo.archived,
			created_at: repo.created_at,
			pushed_at: repo.pushed_at,
			updated_at: repo.updated_at,
			size: repo.size,
			license: repo.license?.spdx_id ?? null,
			...(extra ?? {}),
		},
	};
}

/** Parse a GitHub repo search response. */
function parseRepoResponse(body: unknown): { items: GitHubRepo[]; total: number } {
	const data = body as GitHubSearchResponse;
	return { items: data.items ?? [], total: Math.min(data.total_count ?? 0, MAX_RESULTS) };
}

/**
 * Build common headers for GitHub API requests.
 * Uses GITHUB_TOKEN env var if available for higher rate limits.
 */
function buildFetchInit(): RequestInit {
	const headers: Record<string, string> = {
		Accept: "application/vnd.github+json",
		"X-GitHub-Api-Version": "2022-11-28",
	};

	const token = process.env["GITHUB_TOKEN"];
	if (token) {
		headers["Authorization"] = `Bearer ${token}`;
	}

	return { headers };
}

// ─── Source factory ────────────────────────────────────────────────────────────

export function createGitHubSource(cache: Cache, opts: GitHubSourceOptions = {}): Source {
	const repoQueries = opts.repoQueries ?? DEFAULT_REPO_QUERIES;

	const fetcher = new ThrottledFetcher({ requestsPerSecond: REQUESTS_PER_SECOND });
	const fetchInit = buildFetchInit();

	async function fetchQuery(
		term: string,
	): Promise<{ url: string; hint: string; id: string; metadata: Record<string, unknown> }[]> {
		const maxPages = Math.ceil(MAX_RESULTS / PAGE_SIZE);

		const { items } = await paginate<GitHubRepo>({
			fetcher,
			cache,
			offline: opts.offline,
			fetchInit,
			maxPages,
			buildUrl: (page) =>
				`${GITHUB_API}/search/repositories?q=${encodeURIComponent(term)}&per_page=${PAGE_SIZE}&page=${page}&sort=stars&order=desc`,
			parse: parseRepoResponse,
		});

		return items.map((repo) => repoToCandidate(repo, "repo", term));
	}

	return {
		name: "github",
		source: EntrySource.GitHubSearch,
		displayName: "GitHub",
		priority: 1,
		suggestedCategory: null,

		normalizeUrl(url: string): string {
			return url;
		},

		extractId(url: string): string {
			const ghMatch = url.match(/github\.com\/([^/]+\/[^/]+)/);
			if (ghMatch?.[1]) return ghMatch[1].replace("/", "-");
			return url.split("/").filter(Boolean).pop() ?? url;
		},

		getPopularityValue(entry: CategorizedEntry): number {
			const meta = entry.metadata as Record<string, unknown>;
			return (meta["stars"] as number) ?? 0;
		},

		formatPopularity(entry: CategorizedEntry): string {
			const stars = this.getPopularityValue(entry);
			if (stars > 0) {
				return `\u2B50${formatKNumber(stars)}`;
			}
			return "";
		},

		async discover(writer: DiscoveryWriter): Promise<void> {
			for (const term of repoQueries) {
				process.stderr.write(`[github] 🔍 "${term}"...\n`);
				try {
					const results = await fetchQuery(term);
					const { written } = writeRaw("github", EntrySource.GitHubSearch, results, writer);
					process.stderr.write(`[github] → ${results.length} fetched, ${written} written\n`);
				} catch (err) {
					process.stderr.write(`[github] ⚠️  Failed: ${err}\n`);
				}
			}
		},
	};
}
