"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GiteaTicketTrackerClient = void 0;
// biome-ignore-all lint/style/useNamingConvention lint/nursery/noExcessiveLinesPerFile: Gitea REST payloads mirror upstream field names; this provider client is intentionally centralized.
const catalog_1 = require("../../i18n/catalog");
const constants_1 = require("../constants");
const auth_1 = require("../gitea/auth");
const project_ref_1 = require("../gitea/project-ref");
const label_workflow_state_store_1 = require("./label-workflow-state-store");
const errors_1 = require("./errors");
const gitea_ticket_tracker_client_support_1 = require("./gitea-ticket-tracker-client-support");
const issue_normalization_1 = require("./issue-normalization");
const issue_search_1 = require("./issue-search");
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");
const GITEA_ISSUE_LIST_TYPE_ALL = "all";
const GITEA_ISSUE_LIST_TYPE_ISSUES = "issues";
class GiteaTicketTrackerClient {
    locale;
    project;
    apiBaseUrl;
    repoApiBaseUrl;
    authorizationHeader;
    workflowStateStore;
    requiredWorkflowLabels;
    resolvedWorkflowLabels;
    commentReactions;
    repositoryOwnerKindPromise = null;
    constructor(target, credential, locale = catalog_1.DEFAULT_APP_LOCALE) {
        this.locale = locale;
        this.project = (0, project_ref_1.parseGiteaProjectRef)(target.projectRef, this.locale);
        this.commentReactions = [
            ...(target.commentReactions ?? comment_reactions_1.DEFAULT_TRACKER_COMMENT_REACTIONS),
        ];
        this.apiBaseUrl = `${this.project.origin}/api/v1`;
        this.repoApiBaseUrl = `${this.apiBaseUrl}/repos/${encodeURIComponent(this.project.owner)}/${encodeURIComponent(this.project.repo)}`;
        this.authorizationHeader = (0, auth_1.toGiteaAuthorizationHeader)(credential);
        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);
        return (0, issue_normalization_1.collectMappedTrackerIssues)((await this.listIssueRecords(normalizedQuery)).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 this.collectIssueList({
            state: normalizedQuery.state ?? constants_1.PLATFORM_ISSUE_LIST_STATES.open,
            since: normalizedQuery.since,
            labels: normalizedQuery.labelNames?.join(","),
        });
        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 issues = await this.collectPaginatedArray(`${this.repoApiBaseUrl}/issues`, {
            state: query.state ?? constants_1.PLATFORM_ISSUE_LIST_STATES.open,
            q: search.searchTerm,
            type: query.includePullRequests === true
                ? undefined
                : GITEA_ISSUE_LIST_TYPE_ISSUES,
        }, {
            acceptItem: (issue) => {
                if (query.includePullRequests !== true && (0, gitea_ticket_tracker_client_support_1.isPullRequestIssue)(issue)) {
                    return false;
                }
                return (!(0, issue_search_1.shouldFilterIssueSearchTitle)(search) ||
                    (0, issue_search_1.matchesIssueSearchTitle)(issue.title, search));
            },
        });
        const issueRecords = await Promise.all(issues.map((issue) => this.mapIssueRecord(issue, { includeSourceBranchName: false })));
        return (0, issue_search_1.limitIssueSearchResults)((0, issue_search_1.sortIssueSearchResultsByUpdatedAtDesc)(issueRecords), search);
    }
    async listServiceManagedDeltaIssues(query) {
        const normalizedQuery = (0, issue_normalization_1.normalizeServiceManagedDeltaIssueListQuery)(query);
        const issuesByNumber = new Map();
        for (const labelName of (0, workflow_labels_1.getNonFollowupManagedLabelNames)(this.resolvedWorkflowLabels)) {
            const issues = await this.collectIssueList({
                state: normalizedQuery.state ?? "all",
                since: normalizedQuery.since,
                labels: labelName,
            });
            for (const issue of issues) {
                const issueNumber = (0, gitea_ticket_tracker_client_support_1.resolveIssueNumber)(issue);
                if (issueNumber === null) {
                    continue;
                }
                issuesByNumber.set(String(issueNumber), issue);
            }
        }
        const mappedIssues = await Promise.all([...issuesByNumber.values()].map((issue) => this.mapIssue(issue, {
            requireWorkflowState: false,
            includeSourceBranchName: normalizedQuery.includeSourceBranchName,
        })));
        return (0, issue_normalization_1.collectMappedTrackerIssues)(mappedIssues);
    }
    async getIssue(issueId) {
        const issue = await this.resolveIssueByNumber(Number(issueId));
        const mapped = await this.mapIssue(issue, {
            requireWorkflowState: true,
            includeSourceBranchName: true,
        });
        if (!mapped) {
            throw (0, errors_1.createTrackerMissingWorkflowLabelSetError)(this.locale, issueId);
        }
        return mapped;
    }
    async getIssueRecord(issueId) {
        const issue = await this.resolveIssueByNumber(Number(issueId));
        return this.mapIssueRecord(issue, { 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, gitea_ticket_tracker_client_support_1.isNotFoundError)(error)) {
                return null;
            }
            throw error;
        }
    }
    async getIssueReference(issueId) {
        const issue = await this.resolveIssueByNumber(Number(issueId));
        const issueNumber = (0, gitea_ticket_tracker_client_support_1.resolveIssueNumber)(issue);
        if (issueNumber === null) {
            throw new Error(`Issue reference is unavailable for #${issueId}.`);
        }
        return {
            id: String(issueNumber),
            displayId: `#${issueNumber}`,
            title: issue.title,
            baseTitle: (0, title_progress_1.stripIssueTitleProgressPrefix)(issue.title),
            isOpen: issue.state === constants_1.PLATFORM_ISSUE_STATES.open,
        };
    }
    async getIssueCleanupState(issueId) {
        const issue = await this.resolveIssueByNumber(Number(issueId));
        return {
            isOpen: issue.state === constants_1.PLATFORM_ISSUE_STATES.open,
        };
    }
    async createIssue(input) {
        const milestoneId = await this.resolveIssueMilestoneId(input.milestoneRef ?? null);
        const labelIds = await this.resolveIssueLabelIds(input.labelNames);
        const response = await this.requestJson("POST", `${this.repoApiBaseUrl}/issues`, {
            title: input.title,
            body: input.body,
            assignees: input.assignees,
            ...(milestoneId !== null ? { milestone: milestoneId } : {}),
            ...(labelIds.length > 0 ? { labels: labelIds } : {}),
        });
        const issueNumber = (0, gitea_ticket_tracker_client_support_1.resolveIssueNumber)(response);
        if (issueNumber === null) {
            throw new Error("Created issue index is unavailable.");
        }
        await this.syncIssueProjectRefBestEffort(String(issueNumber), input.projectRef);
        const issue = await this.resolveIssueByNumber(issueNumber);
        return (0, issue_normalization_1.mapMutatedTrackerIssueOrThrow)({
            issue,
            issueId: String(issueNumber),
            mapIssue: (mappedIssue, options) => this.mapIssue(mappedIssue, options),
            createMissingWorkflowLabelSetError: (mappedIssueId) => (0, errors_1.createTrackerMissingWorkflowLabelSetError)(this.locale, mappedIssueId),
        });
    }
    async patchIssueRecord(issueId, input) {
        const patchBody = {};
        let labelIds;
        if (input.title !== undefined && input.title !== null) {
            patchBody.title = input.title;
        }
        if (input.body !== undefined) {
            patchBody.body = input.body;
        }
        if (input.assignees !== undefined) {
            patchBody.assignees = input.assignees;
        }
        if (input.milestoneRef !== undefined && input.milestoneRef !== null) {
            patchBody.milestone = await this.resolveIssueMilestoneId(input.milestoneRef);
        }
        if (input.labelNames !== undefined) {
            labelIds = await this.resolveIssueLabelIds(input.labelNames);
        }
        if (Object.keys(patchBody).length > 0) {
            await this.requestFormUrlEncoded("PATCH", `${this.repoApiBaseUrl}/issues/${issueId}`, patchBody);
        }
        if (labelIds !== undefined) {
            await this.requestJson("PUT", `${this.repoApiBaseUrl}/issues/${issueId}/labels`, { labels: labelIds });
        }
        await this.syncIssueProjectRefBestEffort(issueId, input.projectRef);
        const issue = await this.resolveIssueByNumber(Number(issueId));
        return this.mapIssueRecord(issue, { includeSourceBranchName: true });
    }
    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) {
        try {
            await this.requestFormUrlEncoded("PATCH", `${this.repoApiBaseUrl}/issues/${issueId}`, {
                state: constants_1.PLATFORM_ISSUE_STATES.closed,
            });
        }
        catch (error) {
            if ((0, gitea_ticket_tracker_client_support_1.isAlreadyMergedGiteaError)(error)) {
                return;
            }
            throw error;
        }
    }
    async listBlockingDependencies(issueId) {
        const response = await this.requestJson("GET", `${this.repoApiBaseUrl}/issues/${issueId}/dependencies`);
        const dependencies = Array.isArray(response)
            ? response
            : (0, gitea_ticket_tracker_client_support_1.collectDependencyList)(response);
        return dependencies.flatMap((issue) => {
            const issueNumber = (0, gitea_ticket_tracker_client_support_1.resolveIssueNumber)(issue);
            if (issueNumber === null) {
                return [];
            }
            const dependency = {
                id: String(issueNumber),
                displayId: `#${issueNumber}`,
                title: (0, gitea_ticket_tracker_client_support_1.resolveString)(issue.title) ?? `Issue ${issueNumber}`,
                url: (0, gitea_ticket_tracker_client_support_1.resolveString)(issue.html_url) ?? "",
                isOpen: (0, gitea_ticket_tracker_client_support_1.resolveString)(issue.state) === constants_1.PLATFORM_ISSUE_STATES.open,
            };
            const updatedAt = (0, gitea_ticket_tracker_client_support_1.resolveString)(issue.updated_at);
            if (updatedAt !== null) {
                dependency.updatedAt = updatedAt;
            }
            return [dependency];
        });
    }
    async addBlockingDependency(issueId, blockedByIssueId) {
        await this.requestJson("POST", `${this.repoApiBaseUrl}/issues/${issueId}/dependencies`, {
            owner: this.project.owner,
            repo: this.project.repo,
            index: Number(blockedByIssueId),
        });
    }
    async removeBlockingDependency(issueId, blockedByIssueId) {
        await this.requestJson("DELETE", `${this.repoApiBaseUrl}/issues/${issueId}/dependencies`, {
            owner: this.project.owner,
            repo: this.project.repo,
            index: Number(blockedByIssueId),
        });
    }
    async getAuthenticatedUserLogin() {
        const user = await this.requestJson("GET", `${this.apiBaseUrl}/user`);
        const login = user.login?.trim() ?? user.username?.trim() ?? "";
        if (!login) {
            throw new Error("Authenticated Gitea 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.requestJson("GET", `${this.apiBaseUrl}/orgs/${encodeURIComponent(this.project.owner)}/members/${encodeURIComponent(login)}`);
            return "member";
        }
        catch (error) {
            if ((0, gitea_ticket_tracker_client_support_1.isNotFoundError)(error)) {
                return "non_member";
            }
            return "unknown";
        }
    }
    async getProjectMembership(login) {
        return this.getOrganizationMembership(login);
    }
    async loadRepositoryOwnerKind() {
        try {
            await this.requestJson("GET", `${this.apiBaseUrl}/orgs/${encodeURIComponent(this.project.owner)}`);
            return "organization";
        }
        catch (error) {
            if (!(0, gitea_ticket_tracker_client_support_1.isNotFoundError)(error)) {
                throw error;
            }
        }
        await this.requestJson("GET", `${this.apiBaseUrl}/users/${encodeURIComponent(this.project.owner)}`);
        return "user";
    }
    async listComments(issueId, options = {}) {
        return (0, gitea_ticket_tracker_client_support_1.listGiteaComments)({
            issueId,
            options,
            locale: this.locale,
            repoApiBaseUrl: this.repoApiBaseUrl,
            resolveIssueByNumber: (issueNumber) => this.resolveIssueByNumber(issueNumber),
            collectPaginatedArray: (url, query) => this.collectPaginatedArray(url, query),
            requestJson: (method, url, body) => this.requestJson(method, url, body),
        });
    }
    async acknowledgeComment(comment) {
        await (0, gitea_ticket_tracker_client_support_1.acknowledgeGiteaComment)({
            comment,
            commentReactions: this.commentReactions,
            repoApiBaseUrl: this.repoApiBaseUrl,
            requestJson: (method, url, body) => this.requestJson(method, url, body),
        });
    }
    async createComment(issueId, body) {
        return (0, gitea_ticket_tracker_client_support_1.createGiteaTrackerComment)({
            issueId,
            body,
            locale: this.locale,
            repoApiBaseUrl: this.repoApiBaseUrl,
            requestFormUrlEncoded: (method, url, requestBody) => this.requestFormUrlEncoded(method, url, requestBody),
        });
    }
    async updateComment(commentId, body) {
        return (0, gitea_ticket_tracker_client_support_1.updateGiteaTrackerComment)({
            commentId,
            body,
            locale: this.locale,
            repoApiBaseUrl: this.repoApiBaseUrl,
            requestFormUrlEncoded: (method, url, requestBody) => this.requestFormUrlEncoded(method, url, requestBody),
        });
    }
    async replaceAssignees(issueId, assignees) {
        await this.requestFormUrlEncoded("PATCH", `${this.repoApiBaseUrl}/issues/${issueId}`, {
            assignees,
        });
    }
    async updateIssueTitle(issueId, title) {
        await this.requestFormUrlEncoded("PATCH", `${this.repoApiBaseUrl}/issues/${issueId}`, {
            title,
        });
    }
    resetVolatileIssueReads(_issueId) { }
    async syncWorkflowState(input) {
        await (0, gitea_ticket_tracker_client_support_1.syncGiteaWorkflowState)({
            ...input,
            repoApiBaseUrl: this.repoApiBaseUrl,
            resolvedWorkflowLabels: this.resolvedWorkflowLabels,
            resolveIssueByNumber: (issueNumber) => this.resolveIssueByNumber(issueNumber),
            requestJson: (method, url, body) => this.requestJson(method, url, body),
        });
    }
    async ensureWorkflowSchema(options) {
        return (0, gitea_ticket_tracker_client_support_1.ensureGiteaWorkflowSchema)({
            repoApiBaseUrl: this.repoApiBaseUrl,
            requiredWorkflowLabels: this.requiredWorkflowLabels,
            ...(options?.syncMetadata !== undefined
                ? { syncMetadata: options.syncMetadata }
                : {}),
            collectPaginatedArray: (url, query) => this.collectPaginatedArray(url, query),
            requestJson: (method, url, body) => this.requestJson(method, url, body),
        });
    }
    async checkWorkflowSchema() {
        return (0, gitea_ticket_tracker_client_support_1.checkGiteaWorkflowSchema)({
            repoApiBaseUrl: this.repoApiBaseUrl,
            requiredWorkflowLabels: this.requiredWorkflowLabels,
            collectPaginatedArray: (url, query) => this.collectPaginatedArray(url, query),
        });
    }
    async resolveIssueLabelIds(labelNames) {
        if (labelNames.length === 0) {
            return [];
        }
        const labels = await this.collectPaginatedArray(`${this.repoApiBaseUrl}/labels`);
        const labelIdByName = new Map(labels.map((label) => [label.name, label.id]));
        return labelNames.flatMap((name) => {
            const labelId = labelIdByName.get(name);
            return labelId === undefined ? [] : [labelId];
        });
    }
    async resolveIssueMilestoneId(milestoneRef) {
        const normalized = milestoneRef?.trim();
        if (!normalized) {
            return null;
        }
        if (/^\d+$/u.test(normalized)) {
            return Number(normalized);
        }
        const milestones = await this.collectPaginatedArray(`${this.repoApiBaseUrl}/milestones`, { state: "all" });
        const milestone = milestones.find((entry) => entry.title === normalized ||
            String(entry.id) === normalized ||
            String(entry.number ?? "") === normalized);
        return milestone?.id ?? milestone?.number ?? null;
    }
    async resolveIssueByNumber(issueNumber) {
        return this.requestJson("GET", `${this.repoApiBaseUrl}/issues/${issueNumber}`);
    }
    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 issueNumber = (0, gitea_ticket_tracker_client_support_1.resolveIssueNumber)(issue);
        if (issueNumber === null) {
            throw new Error("Issue index is unavailable.");
        }
        const rawLabels = (0, gitea_ticket_tracker_client_support_1.extractLabelNames)(issue.labels);
        const sourceBranchName = options.includeSourceBranchName
            ? await this.resolvePullRequestHeadBranch(issueNumber, issue)
            : null;
        return (0, issue_normalization_1.normalizeTrackerIssueRecord)({
            id: String(issueNumber),
            displayId: `#${issueNumber}`,
            ...((0, gitea_ticket_tracker_client_support_1.isPullRequestIssue)(issue) ? { pullRequestNumber: issueNumber } : {}),
            title: issue.title,
            body: issue.body ?? "",
            url: issue.html_url,
            author: issue.user?.login ?? issue.user?.username ?? null,
            kind: (0, gitea_ticket_tracker_client_support_1.isPullRequestIssue)(issue)
                ? types_1.TRACKER_ITEM_KINDS.pullRequest
                : types_1.TRACKER_ITEM_KINDS.issue,
            ...(sourceBranchName ? { sourceBranchName } : {}),
            ...((issue.milestone?.id ?? issue.milestone?.number) !== undefined &&
                (issue.milestone?.id ?? issue.milestone?.number) !== null
                ? {
                    milestoneRef: String(issue.milestone?.id ?? issue.milestone?.number),
                }
                : {}),
            ...(typeof issue.ref === "string" && issue.ref.length > 0
                ? { projectRef: issue.ref }
                : {}),
            labelNames: rawLabels,
            assignees: (0, gitea_ticket_tracker_client_support_1.extractAssigneeLogins)(issue),
            isOpen: issue.state === constants_1.PLATFORM_ISSUE_STATES.open,
            updatedAt: issue.updated_at,
            completedAt: issue.closed_at ?? null,
        });
    }
    async resolvePullRequestHeadBranch(issueNumber, issue) {
        if (!(0, gitea_ticket_tracker_client_support_1.isPullRequestIssue)(issue)) {
            return null;
        }
        const pullRequest = await this.requestJson("GET", `${this.repoApiBaseUrl}/pulls/${issueNumber}`);
        return pullRequest.head?.ref ?? null;
    }
    async collectPaginatedArray(url, query = {}, options = {}) {
        return (0, gitea_ticket_tracker_client_support_1.collectPaginatedGiteaArray)({
            url,
            query,
            ...options,
            requestJsonWithHeaders: (method, pageUrl, body) => this.requestJsonWithHeaders(method, pageUrl, body),
        });
    }
    async collectIssueList(query) {
        return this.collectPaginatedArray(`${this.repoApiBaseUrl}/issues`, {
            ...query,
            type: GITEA_ISSUE_LIST_TYPE_ALL,
        });
    }
    async requestJson(method, url, body) {
        const { data } = await this.requestJsonWithHeaders(method, url, body);
        return data;
    }
    async requestJsonWithHeaders(method, url, body) {
        const headers = new Headers();
        headers.set("Accept", "application/json");
        headers.set("Authorization", this.authorizationHeader);
        if (body !== undefined) {
            headers.set("Content-Type", "application/json");
        }
        const response = await fetch(url, {
            method,
            headers,
            ...(body === undefined ? {} : { body: JSON.stringify(body) }),
        });
        return {
            data: await this.parseResponse(response),
            headers: response.headers,
        };
    }
    async requestFormUrlEncoded(method, url, body) {
        const formBody = new URLSearchParams();
        for (const [key, value] of Object.entries(body)) {
            if (value === undefined || value === null) {
                continue;
            }
            if (Array.isArray(value)) {
                for (const item of value) {
                    formBody.append(key, String(item));
                }
                continue;
            }
            formBody.set(key, String(value));
        }
        const response = await fetch(url, {
            method,
            headers: {
                Accept: "application/json",
                Authorization: this.authorizationHeader,
                "Content-Type": "application/x-www-form-urlencoded",
            },
            // Work around Gitea corrupting long UTF-8 JSON issue/comment payloads.
            body: formBody.toString(),
        });
        return this.parseResponse(response);
    }
    async syncIssueProjectRefBestEffort(issueId, projectRef) {
        if (projectRef === undefined || projectRef === null) {
            return;
        }
        try {
            await this.requestFormUrlEncoded("PATCH", `${this.repoApiBaseUrl}/issues/${issueId}`, {
                ref: projectRef,
            });
        }
        catch {
            // Best-effort sync should not fail the main issue mutation.
        }
    }
    async parseResponse(response) {
        if (!response.ok) {
            throw await (0, gitea_ticket_tracker_client_support_1.createGiteaApiError)(response);
        }
        if (response.status === 204) {
            return undefined;
        }
        const text = await response.text();
        if (!text) {
            return undefined;
        }
        return JSON.parse(text);
    }
}
exports.GiteaTicketTrackerClient = GiteaTicketTrackerClient;
