import { log } from "../log.js";
import { defineSource } from "../source.js";
import { ensureArray } from "../utilities/template-helpers.js";
import { gitConfigSource } from "./git-config.js";
import { z } from "zod";
import gitUrlParse from "git-url-parse";
import { Octokit } from "octokit";
//#region src/lib/sources/github.ts
const gitHubRepoSchema = z.object({ repository: z.object({
	allowUpdateBranch: z.boolean(),
	archivedAt: z.string().nullable(),
	autoMergeAllowed: z.boolean(),
	closedIssues: z.object({ totalCount: z.number() }),
	closedPullRequests: z.object({ totalCount: z.number() }),
	codeOfConduct: z.object({ name: z.string() }).nullable(),
	contributingGuidelines: z.object({ body: z.string() }).nullable(),
	contributorCount: z.number().optional(),
	createdAt: z.string(),
	databaseId: z.number(),
	defaultBranchRef: z.object({ name: z.string() }).nullable(),
	deleteBranchOnMerge: z.boolean(),
	description: z.string().nullable(),
	discussions: z.object({ totalCount: z.number() }),
	diskUsage: z.number().nullable(),
	forkCount: z.number(),
	forkingAllowed: z.boolean(),
	fundingLinks: z.array(z.object({
		platform: z.string(),
		url: z.string()
	})),
	gitattributes: z.object({ text: z.string().nullable() }).nullable(),
	gitmodules: z.object({ text: z.string().nullable() }).nullable(),
	hasDiscussionsEnabled: z.boolean(),
	hasIssuesEnabled: z.boolean(),
	hasProjectsEnabled: z.boolean(),
	hasSponsorshipsEnabled: z.boolean(),
	hasVulnerabilityAlertsEnabled: z.boolean(),
	hasWikiEnabled: z.boolean(),
	homepageUrl: z.string().nullable(),
	isArchived: z.boolean(),
	isDisabled: z.boolean(),
	isFork: z.boolean(),
	isInOrganization: z.boolean(),
	isMirror: z.boolean(),
	isPrivate: z.boolean(),
	isSecurityPolicyEnabled: z.boolean(),
	isTemplate: z.boolean(),
	languages: z.object({ edges: z.array(z.object({
		node: z.object({ name: z.string() }),
		size: z.number()
	})) }).nullable(),
	latestRelease: z.object({
		createdAt: z.string(),
		releaseAssets: z.object({ nodes: z.array(z.object({ downloadCount: z.number() })) }),
		tagName: z.string()
	}).nullable(),
	licenseInfo: z.object({
		key: z.string(),
		name: z.string(),
		spdxId: z.string().nullable(),
		url: z.string().nullable()
	}).nullable(),
	mergeCommitAllowed: z.boolean(),
	mergeCommitMessage: z.string(),
	mergeCommitTitle: z.string(),
	mergedPullRequests: z.object({ totalCount: z.number() }),
	mirrorUrl: z.string().nullable(),
	name: z.string(),
	nameWithOwner: z.string(),
	openGraphImageUrl: z.string(),
	openIssues: z.object({ totalCount: z.number() }),
	openPullRequests: z.object({ totalCount: z.number() }),
	owner: z.object({
		__typename: z.string(),
		login: z.string()
	}),
	parent: z.object({
		defaultBranchRef: z.object({ name: z.string() }).nullable(),
		name: z.string(),
		nameWithOwner: z.string(),
		owner: z.object({ login: z.string() }),
		url: z.string()
	}).nullable(),
	primaryLanguage: z.object({ name: z.string() }).nullable(),
	pushedAt: z.string().nullable(),
	rebaseMergeAllowed: z.boolean(),
	releases: z.object({ totalCount: z.number() }),
	repositoryTopics: z.object({ nodes: z.array(z.object({ topic: z.object({ name: z.string() }) })) }),
	securityPolicyUrl: z.string().nullable(),
	squashMergeAllowed: z.boolean(),
	squashMergeCommitMessage: z.string(),
	squashMergeCommitTitle: z.string(),
	sshUrl: z.string(),
	stargazerCount: z.number(),
	templateRepository: z.object({
		name: z.string(),
		owner: z.object({ login: z.string() }),
		url: z.string()
	}).nullable(),
	updatedAt: z.string(),
	url: z.string(),
	usesCustomOpenGraphImage: z.boolean(),
	visibility: z.string(),
	vulnerabilityAlerts: z.object({ totalCount: z.number() }).nullable(),
	watchers: z.object({ totalCount: z.number() }),
	webCommitSignoffRequired: z.boolean()
}) });
/**
* Extract a GitHub owner/repo from git config remote URLs.
* Prefers the "origin" remote, falls back to the first GitHub remote found.
*/
function getGitHubRemoteFromConfig(remotes) {
	if (!remotes) return void 0;
	const sorted = Object.entries(remotes).toSorted(([a], [b]) => {
		if (a === "origin") return -1;
		if (b === "origin") return 1;
		return 0;
	});
	for (const [, remote] of sorted) {
		const { url } = remote;
		if (!url) continue;
		try {
			const parsed = gitUrlParse(url);
			if (parsed.source === "github.com" && parsed.owner && parsed.name) return {
				owner: parsed.owner,
				repo: parsed.name
			};
		} catch {}
	}
}
const graphqlQuery = `
	query($owner: String!, $repo: String!) {
		repository(owner: $owner, name: $repo) {
			name
			nameWithOwner
			owner { __typename login }
			url
			description
			homepageUrl
			createdAt
			updatedAt
			pushedAt
			archivedAt
			databaseId
			isArchived
			isDisabled
			isFork
			isInOrganization
			isMirror
			isPrivate
			isTemplate
			visibility
			diskUsage
			stargazerCount
			forkCount
			sshUrl
			hasWikiEnabled
			hasDiscussionsEnabled
			hasIssuesEnabled
			hasProjectsEnabled
			hasSponsorshipsEnabled
			hasVulnerabilityAlertsEnabled
			isSecurityPolicyEnabled
			securityPolicyUrl
			openGraphImageUrl
			usesCustomOpenGraphImage
			autoMergeAllowed
			allowUpdateBranch
			deleteBranchOnMerge
			forkingAllowed
			mergeCommitAllowed
			mergeCommitMessage
			mergeCommitTitle
			mirrorUrl
			rebaseMergeAllowed
			squashMergeAllowed
			squashMergeCommitMessage
			squashMergeCommitTitle
			webCommitSignoffRequired
			codeOfConduct { name }
			contributingGuidelines { body }
			fundingLinks { platform url }
			licenseInfo { key name spdxId url }
			defaultBranchRef { name }
			primaryLanguage { name }
			parent {
				owner { login }
				name
				nameWithOwner
				url
				defaultBranchRef { name }
			}
			templateRepository {
				owner { login }
				name
				url
			}
			gitattributes: object(expression: "HEAD:.gitattributes") {
				... on Blob { text }
			}
			gitmodules: object(expression: "HEAD:.gitmodules") {
				... on Blob { text }
			}
			openIssues: issues(states: OPEN) { totalCount }
			closedIssues: issues(states: CLOSED) { totalCount }
			openPullRequests: pullRequests(states: OPEN) { totalCount }
			closedPullRequests: pullRequests(states: CLOSED) { totalCount }
			mergedPullRequests: pullRequests(states: MERGED) { totalCount }
			discussions { totalCount }
			vulnerabilityAlerts(states: OPEN) { totalCount }
			watchers { totalCount }
			releases { totalCount }
			latestRelease {
				tagName
				createdAt
				releaseAssets(first: 100) {
					nodes { downloadCount }
				}
			}
			repositoryTopics(first: 50) {
				nodes { topic { name } }
			}
			languages(first: 20, orderBy: { field: SIZE, direction: DESC }) {
				edges {
					node { name }
					size
				}
			}
		}
	}
`;
async function checkHasPages(octokit, owner, repo) {
	try {
		return (await octokit.rest.repos.get({
			owner,
			repo
		})).data.has_pages;
	} catch {
		return false;
	}
}
async function getUpstreamComparison(octokit, owner, repo, defaultBranch, parent) {
	const parentBranch = parent.defaultBranchRef?.name;
	if (!parentBranch) return void 0;
	try {
		const response = await octokit.rest.repos.compareCommitsWithBasehead({
			basehead: `${parent.owner.login}:${parentBranch}...${owner}:${defaultBranch}`,
			owner,
			repo
		});
		return {
			ahead: response.data.ahead_by,
			behind: response.data.behind_by
		};
	} catch {
		return;
	}
}
function countSubmodules(gitmodulesText) {
	if (!gitmodulesText) return 0;
	return gitmodulesText.match(/\[submodule\s/g)?.length ?? 0;
}
function detectLfs(gitattributesText) {
	if (!gitattributesText) return false;
	return gitattributesText.includes("filter=lfs");
}
function extractLanguages(data) {
	const languages = {};
	if (data.languages?.edges) for (const edge of data.languages.edges) languages[edge.node.name] = edge.size;
	return languages;
}
function mapRepoData(data, extras) {
	const releaseDownloadCount = (data.latestRelease?.releaseAssets.nodes.reduce((sum, asset) => sum + asset.downloadCount, 0) ?? 0) || void 0;
	return {
		archivedAt: data.archivedAt ?? void 0,
		codeOfConduct: data.codeOfConduct?.name ?? void 0,
		commitsAheadUpstream: extras.commitsAheadUpstream,
		commitsBehindUpstream: extras.commitsBehindUpstream,
		contributorCount: data.contributorCount,
		createdAt: data.createdAt,
		databaseId: data.databaseId,
		defaultBranch: data.defaultBranchRef?.name ?? void 0,
		description: data.description ?? void 0,
		discussionCount: data.discussions.totalCount,
		diskUsageBytes: data.diskUsage === null ? void 0 : data.diskUsage * 1e3,
		forkCount: data.forkCount,
		forkedFrom: data.parent?.url ?? void 0,
		fundingLinks: data.fundingLinks.length > 0 ? data.fundingLinks.map((link) => ({
			platform: link.platform,
			url: link.url
		})) : void 0,
		hasContributing: data.contributingGuidelines !== null,
		hasDiscussionsEnabled: data.hasDiscussionsEnabled,
		hasIssuesEnabled: data.hasIssuesEnabled,
		hasLfs: detectLfs(data.gitattributes?.text ?? void 0),
		hasPages: extras.hasPages,
		hasProjectsEnabled: data.hasProjectsEnabled,
		hasSponsorshipsEnabled: data.hasSponsorshipsEnabled,
		hasVulnerabilityAlertsEnabled: data.hasVulnerabilityAlertsEnabled,
		hasWikiEnabled: data.hasWikiEnabled,
		homepageUrl: data.homepageUrl === "" ? void 0 : data.homepageUrl ?? void 0,
		isArchived: data.isArchived,
		isDisabled: data.isDisabled,
		isFork: data.isFork,
		isInOrganization: data.isInOrganization,
		isMirror: data.isMirror,
		isPrivate: data.isPrivate,
		isSecurityPolicyEnabled: data.isSecurityPolicyEnabled,
		issueCountClosed: data.closedIssues.totalCount,
		issueCountOpen: data.openIssues.totalCount,
		isTemplate: data.isTemplate,
		languages: extractLanguages(data),
		licenseKey: data.licenseInfo?.key ?? void 0,
		licenseName: data.licenseInfo?.name ?? void 0,
		licenseSpdxId: data.licenseInfo?.spdxId === "NOASSERTION" ? void 0 : data.licenseInfo?.spdxId ?? void 0,
		licenseUrl: data.licenseInfo?.url ?? void 0,
		mirrorUrl: data.mirrorUrl ?? void 0,
		name: data.name,
		nameWithOwner: data.nameWithOwner,
		openGraphImageUrl: data.openGraphImageUrl,
		ownerLogin: data.owner.login,
		ownerType: data.owner.__typename,
		parentNameWithOwner: data.parent?.nameWithOwner ?? void 0,
		primaryLanguage: data.primaryLanguage?.name ?? void 0,
		pullRequestCountClosed: data.closedPullRequests.totalCount,
		pullRequestCountMerged: data.mergedPullRequests.totalCount,
		pullRequestCountOpen: data.openPullRequests.totalCount,
		pushedAt: data.pushedAt ?? void 0,
		releaseCount: data.releases.totalCount || void 0,
		releaseDateLatest: data.latestRelease?.createdAt ?? void 0,
		releaseDownloadCount,
		releaseVersionLatest: data.latestRelease?.tagName ?? void 0,
		securityPolicyUrl: data.securityPolicyUrl ?? void 0,
		settings: {
			allowUpdateBranch: data.allowUpdateBranch,
			autoMergeAllowed: data.autoMergeAllowed,
			deleteBranchOnMerge: data.deleteBranchOnMerge,
			forkingAllowed: data.forkingAllowed,
			mergeCommitAllowed: data.mergeCommitAllowed,
			mergeCommitMessage: data.mergeCommitMessage,
			mergeCommitTitle: data.mergeCommitTitle,
			rebaseMergeAllowed: data.rebaseMergeAllowed,
			squashMergeAllowed: data.squashMergeAllowed,
			squashMergeCommitMessage: data.squashMergeCommitMessage,
			squashMergeCommitTitle: data.squashMergeCommitTitle,
			webCommitSignoffRequired: data.webCommitSignoffRequired
		},
		sshUrl: data.sshUrl,
		stargazerCount: data.stargazerCount,
		submoduleCount: countSubmodules(data.gitmodules?.text ?? void 0),
		templateFrom: data.templateRepository?.url ?? void 0,
		topics: data.repositoryTopics.nodes.map((n) => n.topic.name),
		updatedAt: data.updatedAt,
		url: data.url,
		usesCustomOpenGraphImage: data.usesCustomOpenGraphImage,
		visibility: data.visibility,
		vulnerabilityAlertCount: data.vulnerabilityAlerts?.totalCount ?? void 0,
		watcherCount: data.watchers.totalCount
	};
}
const githubSource = defineSource({
	async discover(context) {
		if (context.options.offline) {
			log.debug("Skipping GitHub data source since we're in offline mode");
			return [];
		}
		let gitRemotes = ensureArray(context.metadata?.gitConfig).map((config) => config.data.remote).filter((remote) => remote !== void 0);
		if (gitRemotes.length === 0 && !context.completedSources?.has("gitConfig")) {
			log.debug(`Missing gitConfig in source context metadata for ${context.options.path}, extracting it now...`);
			gitRemotes = ensureArray(await gitConfigSource.extract(context)).map((config) => config.data.remote).filter((remote) => remote !== void 0);
		}
		return [...new Set(gitRemotes.map((config) => getGitHubRemoteFromConfig(config)).filter((remote) => remote !== void 0).map((remote) => `${remote.owner}/${remote.repo}`))];
	},
	key: "github",
	async parse(input, context) {
		log.debug("Extracting GitHub metadata...");
		const [owner, repo] = input.split("/");
		const octokit = new Octokit(context.options.credentials?.githubToken ? { auth: context.options.credentials.githubToken } : void 0);
		const [graphqlResult, hasPages] = await Promise.all([octokit.graphql(graphqlQuery, {
			owner,
			repo
		}), checkHasPages(octokit, owner, repo)]);
		const data = gitHubRepoSchema.parse(graphqlResult).repository;
		let commitsAheadUpstream;
		let commitsBehindUpstream;
		if (data.isFork && data.parent && data.defaultBranchRef) {
			const comparison = await getUpstreamComparison(octokit, owner, repo, data.defaultBranchRef.name, data.parent);
			commitsAheadUpstream = comparison?.ahead;
			commitsBehindUpstream = comparison?.behind;
		}
		return {
			data: mapRepoData(data, {
				commitsAheadUpstream,
				commitsBehindUpstream,
				hasPages
			}),
			source: `https://github.com/${owner}/${repo}`
		};
	},
	phase: 2
});
//#endregion
export { githubSource };
