"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.OctokitGitHubTicketTrackerClient = void 0;
const catalog_1 = require("../../i18n/catalog");
const constants_1 = require("../constants");
const project_ref_1 = require("../github/project-ref");
const label_workflow_state_store_1 = require("./label-workflow-state-store");
const errors_1 = require("./errors");
const issue_normalization_1 = require("./issue-normalization");
const issue_search_1 = require("./issue-search");
const octokit_github_ticket_tracker_client_support_1 = require("./octokit-github-ticket-tracker-client-support");
const title_progress_1 = require("./title-progress");
const types_1 = require("./types");
const workflow_labels_1 = require("./workflow-labels");
const comment_reactions_1 = require("./comment-reactions");
class OctokitGitHubTicketTrackerClient {
    octokit;
    locale;
    workflowStateStore;
    requiredWorkflowLabels;
    resolvedWorkflowLabels;
    commentReactions;
    project;
    repositoryOwnerKindPromise = null;
    constructor(target, octokit, locale = catalog_1.DEFAULT_APP_LOCALE) {
        this.octokit = octokit;
        this.locale = locale;
        this.project = (0, project_ref_1.parseGitHubProjectRef)(target.projectRef);
        this.commentReactions = [
            ...(target.commentReactions ?? comment_reactions_1.DEFAULT_TRACKER_COMMENT_REACTIONS),
        ];
        this.resolvedWorkflowLabels = (0, workflow_labels_1.resolveWorkflowLabels)(target.workflowLabels);
        this.workflowStateStore = new label_workflow_state_store_1.LabelWorkflowStateStore(this.resolvedWorkflowLabels, this.locale);
        this.requiredWorkflowLabels = (0, workflow_labels_1.buildManagedLabelDefinitions)({
            workflowLabels: this.resolvedWorkflowLabels,
            locale: this.locale,
        });
    }
    async listIssues(query) {
        const normalizedQuery = (0, issue_normalization_1.normalizeTrackerIssueListQuery)(query);
        const issueRecords = await this.listIssueRecords(normalizedQuery);
        return (0, issue_normalization_1.collectMappedTrackerIssues)(issueRecords.map((issueRecord) => (0, issue_normalization_1.normalizeMappedTrackerIssueRecord)({
            issueRecord,
            requireWorkflowState: true,
            parseWorkflowState: (labelNames) => this.workflowStateStore.parse(labelNames),
        })), normalizedQuery.workflowStatus);
    }
    async listIssueRecords(query) {
        const normalizedQuery = (0, issue_normalization_1.normalizeTrackerIssueListQuery)(query);
        const issues = await (0, octokit_github_ticket_tracker_client_support_1.listGitHubTrackerIssues)({
            octokit: this.octokit,
            owner: this.project.owner,
            repo: this.project.repo,
            state: normalizedQuery.state ?? constants_1.PLATFORM_ISSUE_LIST_STATES.open,
            ...(normalizedQuery.since !== undefined
                ? { since: normalizedQuery.since }
                : {}),
            ...(normalizedQuery.labelNames !== undefined
                ? { labelNames: normalizedQuery.labelNames }
                : {}),
        });
        return Promise.all(issues.map((issue) => this.mapIssueRecord(issue, {
            includeSourceBranchName: normalizedQuery.includeSourceBranchName,
        })));
    }
    async searchIssueRecords(query) {
        const search = (0, issue_search_1.normalizeIssueSearchQuery)(query);
        if (search === null) {
            return [];
        }
        const matchedIssues = [];
        for (let page = 1;; page += 1) {
            const response = await this.octokit.search.issuesAndPullRequests({
                q: buildGitHubIssueRecordSearchQuery({
                    owner: this.project.owner,
                    repo: this.project.repo,
                    state: query.state ?? constants_1.PLATFORM_ISSUE_LIST_STATES.open,
                    mode: search.mode,
                    searchTerm: search.searchTerm,
                    includePullRequests: query.includePullRequests ?? false,
                }),
                sort: "updated",
                order: "desc",
                // biome-ignore lint/style/useNamingConvention: GitHub REST parameter name.
                per_page: types_1.TRACKER_PROVIDER_REST_PAGE_LIMIT,
                page,
            });
            const issues = response.data.items;
            for (const issue of issues) {
                if (query.includePullRequests !== true && issue.pull_request) {
                    continue;
                }
                if (!(0, issue_search_1.shouldFilterIssueSearchTitle)(search) ||
                    (0, issue_search_1.matchesIssueSearchTitle)(issue.title, search)) {
                    matchedIssues.push(issue);
                }
                if ((0, issue_search_1.isIssueSearchLimitReached)(search, matchedIssues.length)) {
                    break;
                }
            }
            if (issues.length < types_1.TRACKER_PROVIDER_REST_PAGE_LIMIT ||
                (0, issue_search_1.isIssueSearchLimitReached)(search, matchedIssues.length)) {
                break;
            }
        }
        const limitedIssues = (0, issue_search_1.limitIssueSearchResults)(matchedIssues, search);
        return Promise.all(limitedIssues.map((issue) => this.mapIssueRecord(issue, {
            includeSourceBranchName: false,
        })));
    }
    async listServiceManagedDeltaIssues(query) {
        return (0, octokit_github_ticket_tracker_client_support_1.listGitHubServiceManagedDeltaIssues)({
            octokit: this.octokit,
            owner: this.project.owner,
            repo: this.project.repo,
            query,
            labelNames: (0, workflow_labels_1.getNonFollowupManagedLabelNames)(this.resolvedWorkflowLabels),
            mapIssue: (issue, options) => this.mapIssue(issue, options),
        });
    }
    async getIssue(issueId) {
        const response = await this.octokit.issues.get({
            owner: this.project.owner,
            repo: this.project.repo,
            // biome-ignore lint/style/useNamingConvention: GitHub REST parameter name.
            issue_number: Number(issueId),
        });
        const mapped = await this.mapIssue(response.data, {
            requireWorkflowState: true,
            includeSourceBranchName: true,
        });
        if (!mapped) {
            throw (0, errors_1.createTrackerMissingWorkflowLabelSetError)(this.locale, issueId);
        }
        return mapped;
    }
    async getIssueRecord(issueId) {
        const response = await this.octokit.issues.get({
            owner: this.project.owner,
            repo: this.project.repo,
            // biome-ignore lint/style/useNamingConvention: GitHub REST parameter name.
            issue_number: Number(issueId),
        });
        return this.mapIssueRecord(response.data, {
            includeSourceBranchName: true,
        });
    }
    async getPullRequestThreadRecord(input) {
        try {
            const issue = await this.getIssueRecord(String(input.pullRequestNumber));
            return issue.kind === types_1.TRACKER_ITEM_KINDS.pullRequest ? issue : null;
        }
        catch (error) {
            if ((0, octokit_github_ticket_tracker_client_support_1.isNotFoundError)(error)) {
                return null;
            }
            throw error;
        }
    }
    async getIssueReference(issueId) {
        const response = await this.octokit.issues.get({
            owner: this.project.owner,
            repo: this.project.repo,
            // biome-ignore lint/style/useNamingConvention: GitHub REST parameter name.
            issue_number: Number(issueId),
        });
        const issue = response.data;
        return {
            id: String(issue.number),
            displayId: `#${issue.number}`,
            title: issue.title,
            baseTitle: (0, title_progress_1.stripIssueTitleProgressPrefix)(issue.title),
            isOpen: issue.state === constants_1.PLATFORM_ISSUE_STATES.open,
        };
    }
    async getIssueCleanupState(issueId) {
        const response = await this.octokit.issues.get({
            owner: this.project.owner,
            repo: this.project.repo,
            // biome-ignore lint/style/useNamingConvention: GitHub REST parameter name.
            issue_number: Number(issueId),
        });
        const issue = response.data;
        return {
            isOpen: issue.state === constants_1.PLATFORM_ISSUE_STATES.open,
        };
    }
    async createIssue(input) {
        const milestone = await this.resolveGitHubMilestoneNumber(input.milestoneRef ?? null);
        const response = await this.octokit.issues.create({
            owner: this.project.owner,
            repo: this.project.repo,
            title: input.title,
            body: input.body,
            assignees: input.assignees,
            labels: input.labelNames,
            ...(milestone !== null ? { milestone } : {}),
        });
        return (0, issue_normalization_1.mapMutatedTrackerIssueOrThrow)({
            issue: response.data,
            issueId: String(response.data.number),
            mapIssue: (issue, options) => this.mapIssue(issue, options),
            createMissingWorkflowLabelSetError: (mappedIssueId) => (0, errors_1.createTrackerMissingWorkflowLabelSetError)(this.locale, mappedIssueId),
            afterMap: async (mapped) => {
                await this.syncGitHubProjectBinding({
                    issueNodeId: response.data.node_id ?? null,
                    projectRef: input.projectRef ?? null,
                });
                return this.attachIssueProjectRef(mapped, input.projectRef);
            },
        });
    }
    async patchIssueRecord(issueId, input) {
        const milestone = await this.resolveGitHubMilestoneNumber(input.milestoneRef ?? null);
        const response = await this.octokit.issues.update({
            owner: this.project.owner,
            repo: this.project.repo,
            // biome-ignore lint/style/useNamingConvention: GitHub REST parameter name.
            issue_number: Number(issueId),
            ...(input.title !== undefined && input.title !== null
                ? { title: input.title }
                : {}),
            ...(input.body !== undefined ? { body: input.body } : {}),
            ...(input.assignees !== undefined ? { assignees: input.assignees } : {}),
            ...(input.labelNames !== undefined ? { labels: input.labelNames } : {}),
            ...(milestone !== null ? { milestone } : {}),
        });
        await this.syncGitHubProjectBinding({
            issueNodeId: response.data.node_id ?? null,
            projectRef: input.projectRef ?? null,
        });
        const issue = await this.mapIssueRecord(response.data, { includeSourceBranchName: true });
        return this.attachIssueProjectRef(issue, input.projectRef);
    }
    async patchIssue(issueId, input) {
        const issue = await this.patchIssueRecord(issueId, input);
        const mapped = (0, issue_normalization_1.normalizeMappedTrackerIssueRecord)({
            issueRecord: issue,
            requireWorkflowState: true,
            parseWorkflowState: (labelNames) => this.workflowStateStore.parse(labelNames),
        });
        if (mapped !== null) {
            return mapped;
        }
        throw (0, errors_1.createTrackerMissingWorkflowLabelSetError)(this.locale, issueId);
    }
    async closeIssue(issueId) {
        await this.octokit.issues.update({
            owner: this.project.owner,
            repo: this.project.repo,
            // biome-ignore lint/style/useNamingConvention: GitHub REST parameter name.
            issue_number: Number(issueId),
            state: constants_1.PLATFORM_ISSUE_STATES.closed,
        });
    }
    async listBlockingDependencies(issueId) {
        const response = await this.octokit.request("GET /repos/{owner}/{repo}/issues/{issue_number}/dependencies/blocked_by", {
            owner: this.project.owner,
            repo: this.project.repo,
            // biome-ignore lint/style/useNamingConvention: GitHub REST parameter name.
            issue_number: Number(issueId),
        });
        const dependencies = response.data;
        return dependencies.map((issue) => {
            const dependency = {
                id: String(issue.number),
                displayId: `#${issue.number}`,
                title: issue.title,
                url: issue.html_url,
                isOpen: issue.state === constants_1.PLATFORM_ISSUE_STATES.open,
            };
            if (issue.updated_at !== undefined && issue.updated_at !== null) {
                dependency.updatedAt = issue.updated_at;
            }
            return dependency;
        });
    }
    async addBlockingDependency(issueId, blockedByIssueId) {
        const blockedByGitHubIssueId = await this.resolveGitHubIssueId(blockedByIssueId);
        await this.octokit.request("POST /repos/{owner}/{repo}/issues/{issue_number}/dependencies/blocked_by", {
            owner: this.project.owner,
            repo: this.project.repo,
            // biome-ignore lint/style/useNamingConvention: GitHub REST parameter name.
            issue_number: Number(issueId),
            // biome-ignore lint/style/useNamingConvention: GitHub REST parameter name.
            issue_id: blockedByGitHubIssueId,
        });
    }
    async removeBlockingDependency(issueId, blockedByIssueId) {
        const blockedByGitHubIssueId = await this.resolveGitHubIssueId(blockedByIssueId);
        await this.octokit.request("DELETE /repos/{owner}/{repo}/issues/{issue_number}/dependencies/blocked_by/{issue_id}", {
            owner: this.project.owner,
            repo: this.project.repo,
            // biome-ignore lint/style/useNamingConvention: GitHub REST parameter name.
            issue_number: Number(issueId),
            // biome-ignore lint/style/useNamingConvention: GitHub REST parameter name.
            issue_id: blockedByGitHubIssueId,
        });
    }
    async getAuthenticatedUserLogin() {
        const response = await this.octokit.users.getAuthenticated();
        const login = response.data.login?.trim();
        if (!login) {
            throw new Error("Authenticated GitHub user login is unavailable.");
        }
        return login;
    }
    async getRepositoryOwnerKind() {
        const cached = this.repositoryOwnerKindPromise;
        if (cached !== null) {
            return cached;
        }
        const loaded = this.loadRepositoryOwnerKind().catch((error) => {
            this.repositoryOwnerKindPromise = null;
            throw error;
        });
        this.repositoryOwnerKindPromise = loaded;
        return loaded;
    }
    async getOrganizationMembership(login) {
        const ownerKind = await this.getRepositoryOwnerKind();
        if (ownerKind !== "organization") {
            return "member";
        }
        try {
            await this.octokit.request("GET /orgs/{org}/members/{username}", {
                org: this.project.owner,
                username: login,
            });
            return "member";
        }
        catch (error) {
            if ((0, octokit_github_ticket_tracker_client_support_1.isNotFoundError)(error)) {
                return "non_member";
            }
            return "unknown";
        }
    }
    async getProjectMembership(login) {
        return this.getOrganizationMembership(login);
    }
    async loadRepositoryOwnerKind() {
        const ownerType = await this.loadProjectOwnerType();
        if (ownerType === "Organization") {
            return "organization";
        }
        if (ownerType === "User") {
            return "user";
        }
        throw new Error(`GitHub repository owner kind is unavailable for ${this.project.owner}/${this.project.repo}.`);
    }
    async listComments(issueId, options = {}) {
        return (0, octokit_github_ticket_tracker_client_support_1.listGitHubComments)({
            octokit: this.octokit,
            owner: this.project.owner,
            repo: this.project.repo,
            issueId,
            options,
            locale: this.locale,
        });
    }
    async createComment(issueId, body) {
        return (0, octokit_github_ticket_tracker_client_support_1.createGitHubTrackerComment)({
            octokit: this.octokit,
            owner: this.project.owner,
            repo: this.project.repo,
            issueId,
            body,
            locale: this.locale,
        });
    }
    async acknowledgeComment(comment) {
        await (0, octokit_github_ticket_tracker_client_support_1.acknowledgeGitHubComment)({
            octokit: this.octokit,
            owner: this.project.owner,
            repo: this.project.repo,
            comment,
            commentReactions: this.commentReactions,
        });
    }
    async updateComment(commentId, body) {
        return (0, octokit_github_ticket_tracker_client_support_1.updateGitHubTrackerComment)({
            octokit: this.octokit,
            owner: this.project.owner,
            repo: this.project.repo,
            commentId,
            body,
            locale: this.locale,
        });
    }
    async replaceAssignees(issueId, assignees) {
        await this.octokit.issues.update({
            owner: this.project.owner,
            repo: this.project.repo,
            // biome-ignore lint/style/useNamingConvention: GitHub REST parameter name.
            issue_number: Number(issueId),
            assignees,
        });
    }
    async updateIssueTitle(issueId, title) {
        await this.octokit.issues.update({
            owner: this.project.owner,
            repo: this.project.repo,
            // biome-ignore lint/style/useNamingConvention: GitHub REST parameter name.
            issue_number: Number(issueId),
            title,
        });
    }
    resetVolatileIssueReads(_issueId) { }
    async syncWorkflowState(input) {
        await (0, octokit_github_ticket_tracker_client_support_1.syncGitHubWorkflowState)({
            octokit: this.octokit,
            owner: this.project.owner,
            repo: this.project.repo,
            ...input,
            resolvedWorkflowLabels: this.resolvedWorkflowLabels,
        });
    }
    async ensureWorkflowSchema(options) {
        return (0, octokit_github_ticket_tracker_client_support_1.ensureGitHubWorkflowSchema)({
            octokit: this.octokit,
            owner: this.project.owner,
            repo: this.project.repo,
            requiredWorkflowLabels: this.requiredWorkflowLabels,
            ...(options?.syncMetadata !== undefined
                ? { syncMetadata: options.syncMetadata }
                : {}),
        });
    }
    async checkWorkflowSchema() {
        return (0, octokit_github_ticket_tracker_client_support_1.checkGitHubWorkflowSchema)({
            octokit: this.octokit,
            owner: this.project.owner,
            repo: this.project.repo,
            requiredWorkflowLabels: this.requiredWorkflowLabels,
        });
    }
    async resolveGitHubIssueId(issueId) {
        const response = await this.octokit.issues.get({
            owner: this.project.owner,
            repo: this.project.repo,
            // biome-ignore lint/style/useNamingConvention: GitHub REST parameter name.
            issue_number: Number(issueId),
        });
        return response.data.id;
    }
    async resolveGitHubMilestoneNumber(milestoneRef) {
        const normalized = milestoneRef?.trim();
        if (!normalized) {
            return null;
        }
        if (/^\d+$/u.test(normalized)) {
            return Number(normalized);
        }
        const milestones = (await this.octokit.paginate(this.octokit.issues.listMilestones, {
            owner: this.project.owner,
            repo: this.project.repo,
            state: "all",
            // biome-ignore lint/style/useNamingConvention: GitHub REST parameter name.
            per_page: types_1.TRACKER_PROVIDER_REST_PAGE_LIMIT,
        }));
        const milestone = milestones.find((entry) => entry.title === normalized);
        return milestone?.number ?? null;
    }
    async mapIssue(issue, options) {
        return (0, issue_normalization_1.mapNormalizedTrackerIssue)({
            issue,
            options,
            mapIssueRecord: (mappedIssue, mapOptions) => this.mapIssueRecord(mappedIssue, mapOptions),
            parseWorkflowState: (labelNames) => this.workflowStateStore.parse(labelNames),
        });
    }
    async mapIssueRecord(issue, options) {
        const rawLabels = issue.labels.flatMap((label) => typeof label.name === "string" ? [label.name] : []);
        const sourceBranchName = options.includeSourceBranchName
            ? await this.resolvePullRequestHeadBranch(issue)
            : null;
        return (0, issue_normalization_1.normalizeTrackerIssueRecord)({
            id: String(issue.number),
            displayId: `#${issue.number}`,
            ...(issue.pull_request ? { pullRequestNumber: issue.number } : {}),
            title: issue.title,
            body: issue.body ?? "",
            url: issue.html_url,
            author: issue.user?.login ?? null,
            kind: issue.pull_request
                ? types_1.TRACKER_ITEM_KINDS.pullRequest
                : types_1.TRACKER_ITEM_KINDS.issue,
            ...(sourceBranchName ? { sourceBranchName } : {}),
            ...(issue.milestone
                ? {
                    milestoneRef: String(issue.milestone.number),
                }
                : {}),
            labelNames: rawLabels,
            assignees: (issue.assignees ?? []).flatMap((assignee) => assignee.login ? [assignee.login] : []),
            isOpen: issue.state === constants_1.PLATFORM_ISSUE_STATES.open,
            updatedAt: issue.updated_at,
            completedAt: issue.closed_at ?? null,
        });
    }
    async loadProjectOwnerType() {
        const response = await this.octokit.repos.get({
            owner: this.project.owner,
            repo: this.project.repo,
        });
        return response.data.owner?.type ?? null;
    }
    async resolvePullRequestHeadBranch(issue) {
        if (!issue.pull_request) {
            return null;
        }
        const response = await this.octokit.pulls.get({
            owner: this.project.owner,
            repo: this.project.repo,
            // biome-ignore lint/style/useNamingConvention: GitHub REST parameter name.
            pull_number: issue.number,
        });
        return response.data.head.ref;
    }
    async resolveProjectOwnerType() {
        try {
            return await this.loadProjectOwnerType();
        }
        catch {
            return null;
        }
    }
    async syncGitHubProjectBinding(input) {
        const issueNodeId = input.issueNodeId?.trim() ?? "";
        const projectRef = input.projectRef?.trim() ?? "";
        if (!issueNodeId || !projectRef) {
            return;
        }
        try {
            const projectId = await this.resolveGitHubProjectV2Id(projectRef);
            if (projectId === null) {
                return;
            }
            await this.octokit.graphql(`
          mutation AddIssueToProjectV2($projectId: ID!, $contentId: ID!) {
            addProjectV2ItemById(
              input: { projectId: $projectId, contentId: $contentId }
            ) {
              item {
                id
              }
            }
          }
        `, {
                projectId,
                contentId: issueNodeId,
            });
        }
        catch {
            // Best effort only.
        }
    }
    async resolveGitHubProjectV2Id(projectTitle) {
        const ownerType = await this.resolveProjectOwnerType();
        if (ownerType === "Organization") {
            return this.resolveGitHubProjectV2IdByOwnerField("organization", projectTitle);
        }
        if (ownerType === "User") {
            return this.resolveGitHubProjectV2IdByOwnerField("user", projectTitle);
        }
        return null;
    }
    async resolveGitHubProjectV2IdByOwnerField(ownerField, projectTitle) {
        let after = null;
        while (true) {
            const response = await this.octokit.graphql(`
          query GitHubProjectV2Lookup(
            $owner: String!
            $query: String!
            $after: String
          ) {
            ${ownerField}(login: $owner) {
              projectsV2(first: ${types_1.TRACKER_PROVIDER_GRAPHQL_PAGE_LIMIT}, query: $query, after: $after) {
                nodes {
                  id
                  title
                }
                pageInfo {
                  hasNextPage
                  endCursor
                }
              }
            }
          }
        `, {
                owner: this.project.owner,
                query: projectTitle,
                after,
            });
            const owner = ownerField === "organization" ? response.organization : response.user;
            const projects = owner?.projectsV2?.nodes?.flatMap((project) => project?.title === projectTitle ? [project] : []) ?? [];
            if (projects.length > 0) {
                return projects[0]?.id ?? null;
            }
            const pageInfo = owner?.projectsV2?.pageInfo ?? null;
            if (!pageInfo?.hasNextPage || !pageInfo.endCursor) {
                return null;
            }
            after = pageInfo.endCursor;
        }
    }
    attachIssueProjectRef(issue, projectRef) {
        const normalized = projectRef?.trim();
        if (!normalized) {
            return issue;
        }
        return {
            ...issue,
            projectRef: normalized,
        };
    }
}
exports.OctokitGitHubTicketTrackerClient = OctokitGitHubTicketTrackerClient;
function buildGitHubIssueRecordSearchQuery(input) {
    const terms = [
        `repo:${input.owner}/${input.repo}`,
        ...(input.includePullRequests ? [] : ["is:issue"]),
        ...resolveGitHubSearchStateTerms(input.state),
        input.mode === types_1.TRACKER_ISSUE_SEARCH_MODES.title
            ? "in:title"
            : "in:title,body,comments",
        formatGitHubSearchTerm(input.searchTerm),
    ];
    return terms.join(" ");
}
function resolveGitHubSearchStateTerms(state) {
    if (state === constants_1.PLATFORM_ISSUE_LIST_STATES.all) {
        return [];
    }
    return [`state:${state}`];
}
function formatGitHubSearchTerm(term) {
    if (!/[\s"-]/u.test(term)) {
        return term;
    }
    const escaped = term.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"');
    return `"${escaped}"`;
}
