"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GitLabRepositoryHostClient = void 0;
const constants_1 = require("../constants");
const api_support_1 = require("../gitlab/api-support");
const project_ref_1 = require("../gitlab/project-ref");
const constants_2 = require("./constants");
const errors_1 = require("./errors");
const login_support_1 = require("./login-support");
const GITLAB_REPOSITORY_HOST_ERROR_NAME = "GitLabRepositoryHostError";
class GitLabRepositoryHostClient {
    config;
    token;
    projectApiBaseUrl;
    constructor(config, token) {
        this.config = config;
        this.token = token;
        this.projectApiBaseUrl = (0, project_ref_1.buildGitLabProjectApiBaseUrl)(config.projectRef);
    }
    async getRepository() {
        const project = await this.requestJson("GET", this.projectApiBaseUrl);
        return { defaultBranch: project.default_branch ?? "main" };
    }
    async findPullRequestByBranch(branchName) {
        const mergeRequests = await this.collectPaginatedArray(`${this.projectApiBaseUrl}/merge_requests`, {
            state: "all",
            source_branch: branchName,
            order_by: "updated_at",
            sort: "desc",
        });
        const match = [...mergeRequests]
            .sort((left, right) => Number(isOpenMergeRequest(right)) - Number(isOpenMergeRequest(left)))
            .find((mergeRequest) => mergeRequest.source_branch === branchName);
        return match ? mapMergeRequest(match) : null;
    }
    async getPullRequest(pullRequestNumber) {
        const mergeRequest = await this.getMergeRequestOrNull(pullRequestNumber);
        return mergeRequest === null ? null : mapMergeRequest(mergeRequest);
    }
    async getPullRequestAutomationState(pullRequestNumber) {
        const mergeRequest = await this.getMergeRequestOrNull(pullRequestNumber);
        if (mergeRequest === null) {
            return null;
        }
        if (mergeRequest.state !== "opened") {
            return null;
        }
        const approvals = await this.getMergeRequestApprovals(pullRequestNumber);
        const reviewDecision = resolveReviewDecision(mergeRequest, approvals);
        const headRefOid = mergeRequest.sha ?? "";
        return {
            number: mergeRequest.iid,
            url: mergeRequest.web_url,
            reviewDecision,
            mergeability: resolveMergeability(mergeRequest),
            mergeStateStatus: resolveMergeStateStatus(mergeRequest),
            isDraft: isDraftMergeRequest(mergeRequest),
            headRefOid,
            latestApprovedReviewCommitOid: reviewDecision === "APPROVED" ? headRefOid : null,
        };
    }
    async createPullRequest(input) {
        const mergeRequest = await this.requestJson("POST", `${this.projectApiBaseUrl}/merge_requests`, {
            source_branch: input.headBranch,
            target_branch: input.baseBranch,
            title: input.draft ? `Draft: ${input.title}` : input.title,
            description: input.body,
        });
        return mapMergeRequest(mergeRequest);
    }
    async listPullRequestChangedFiles(pullRequestNumber) {
        const diffs = await this.collectPaginatedArray(`${this.projectApiBaseUrl}/merge_requests/${pullRequestNumber}/diffs`, {});
        return diffs.flatMap((diff) => {
            const path = diff.new_path ?? diff.old_path ?? "";
            if (!path) {
                return [];
            }
            return [
                {
                    path,
                    status: resolveDiffStatus(diff),
                },
            ];
        });
    }
    async listPullRequestCommits(pullRequestNumber) {
        const commits = await this.collectPaginatedArray(`${this.projectApiBaseUrl}/merge_requests/${pullRequestNumber}/commits`, {});
        return commits.flatMap((commit) => {
            const oid = commit.id ?? commit.short_id ?? "";
            if (!oid) {
                return [];
            }
            return [
                {
                    oid,
                    title: commit.title ?? firstLine(commit.message ?? ""),
                    authorName: commit.author_name ?? null,
                    authoredAt: commit.authored_date ?? null,
                    url: commit.web_url ?? null,
                },
            ];
        });
    }
    async mergePullRequest(pullRequestNumber, method) {
        try {
            await this.requestJson("PUT", `${this.projectApiBaseUrl}/merge_requests/${pullRequestNumber}/merge`, { squash: method === "squash" });
        }
        catch (error) {
            if (await this.isPullRequestHeadIncludedInBase(pullRequestNumber)) {
                return;
            }
            if (isRetryableGitLabMergeError(error)) {
                throw new errors_1.RetryablePullRequestMergeError(error instanceof Error ? error.message : String(error));
            }
            throw error;
        }
    }
    async isPullRequestHeadIncludedInBase(pullRequestNumber) {
        const mergeRequest = await this.getMergeRequestOrNull(pullRequestNumber);
        const baseBranch = mergeRequest?.target_branch ?? "";
        const headSha = mergeRequest?.sha ?? "";
        if (!baseBranch || !headSha) {
            return false;
        }
        const url = new URL(`${this.projectApiBaseUrl}/repository/compare`);
        url.searchParams.set("from", baseBranch);
        url.searchParams.set("to", headSha);
        const comparison = await this.requestJson("GET", url.toString());
        return (comparison.compare_same_ref === true ||
            (comparison.commits?.length ?? 0) === 0);
    }
    async syncPullRequestAssignees(pullRequestNumber, assignees) {
        const normalizedAssignees = (0, login_support_1.normalizeLogins)(assignees);
        if (normalizedAssignees.length === 0) {
            return;
        }
        const mergeRequest = await this.getMergeRequestOrNull(pullRequestNumber);
        if (mergeRequest === null) {
            return;
        }
        const existingAssignees = mergeRequest.assignees ?? [];
        const assigneesToAdd = (0, login_support_1.findMissingLogins)({
            requestedLogins: normalizedAssignees,
            existingLogins: existingAssignees.map((assignee) => assignee.username),
        });
        if (assigneesToAdd.length === 0) {
            return;
        }
        const assigneeIds = await (0, api_support_1.resolveGitLabUserIds)({
            projectRef: this.config.projectRef,
            token: this.token,
            errorName: GITLAB_REPOSITORY_HOST_ERROR_NAME,
            logins: assigneesToAdd,
        });
        if (assigneeIds.length === 0) {
            return;
        }
        await this.updateMergeRequest(pullRequestNumber, {
            assignee_ids: [
                ...existingAssignees.flatMap((assignee) => assignee.id),
                ...assigneeIds,
            ],
        });
    }
    async syncPullRequestReviewers(pullRequestNumber, reviewers) {
        const normalizedReviewers = (0, login_support_1.normalizeLogins)(reviewers);
        if (normalizedReviewers.length === 0) {
            return { eligibleReviewers: [] };
        }
        const mergeRequest = await this.getMergeRequestOrNull(pullRequestNumber);
        if (mergeRequest === null) {
            return { eligibleReviewers: [] };
        }
        const existingReviewers = mergeRequest.reviewers ?? [];
        const eligibleReviewers = (0, login_support_1.findMissingLogins)({
            requestedLogins: normalizedReviewers,
            existingLogins: [],
            excludedLogins: [mergeRequest.author?.username],
        });
        const reviewersToRequest = (0, login_support_1.findMissingLogins)({
            requestedLogins: eligibleReviewers,
            existingLogins: existingReviewers.map((reviewer) => reviewer.username),
        });
        const existingEligibleReviewers = (0, login_support_1.findMissingLogins)({
            requestedLogins: eligibleReviewers,
            existingLogins: [],
            excludedLogins: reviewersToRequest,
        });
        if (reviewersToRequest.length === 0) {
            return { eligibleReviewers };
        }
        const resolvedReviewers = await (0, api_support_1.resolveGitLabUsersByLogin)({
            projectRef: this.config.projectRef,
            token: this.token,
            errorName: GITLAB_REPOSITORY_HOST_ERROR_NAME,
            logins: reviewersToRequest,
        });
        const assignableReviewers = filterAssignableGitLabReviewers({
            eligibleReviewers,
            existingEligibleReviewers,
            resolvedReviewers,
        });
        if (assignableReviewers.length === 0) {
            return { eligibleReviewers: [] };
        }
        if (resolvedReviewers.length === 0) {
            return { eligibleReviewers: assignableReviewers };
        }
        await this.updateMergeRequest(pullRequestNumber, {
            reviewer_ids: [
                ...existingReviewers.flatMap((reviewer) => reviewer.id),
                ...resolvedReviewers.map((reviewer) => reviewer.id),
            ],
        });
        return { eligibleReviewers: assignableReviewers };
    }
    async markPullRequestReadyForReview(pullRequestNumber) {
        const mergeRequest = await this.getMergeRequestOrNull(pullRequestNumber);
        if (mergeRequest === null || !isDraftMergeRequest(mergeRequest)) {
            return;
        }
        await this.updateMergeRequest(pullRequestNumber, {
            title: mergeRequest.title.replace(/^(?:draft|wip):\s*/iu, ""),
        });
    }
    async getMergeRequestOrNull(pullRequestNumber) {
        try {
            return await this.requestJson("GET", `${this.projectApiBaseUrl}/merge_requests/${pullRequestNumber}`);
        }
        catch (error) {
            if ((0, api_support_1.isGitLabNotFoundError)(error)) {
                return null;
            }
            throw error;
        }
    }
    async getMergeRequestApprovals(pullRequestNumber) {
        try {
            return await this.requestJson("GET", `${this.projectApiBaseUrl}/merge_requests/${pullRequestNumber}/approvals`);
        }
        catch (error) {
            if ((0, api_support_1.isGitLabNotFoundError)(error)) {
                return null;
            }
            throw error;
        }
    }
    async updateMergeRequest(pullRequestNumber, body) {
        await this.requestJson("PUT", `${this.projectApiBaseUrl}/merge_requests/${pullRequestNumber}`, body);
    }
    async collectPaginatedArray(url, query) {
        const results = [];
        let page = 1;
        for (;;) {
            const current = new URL(url);
            for (const [key, value] of Object.entries(query)) {
                current.searchParams.set(key, value);
            }
            current.searchParams.set("page", String(page));
            current.searchParams.set("per_page", String(constants_2.REPOSITORY_HOST_PROVIDER_PAGE_LIMIT));
            const pageResults = await this.requestJson("GET", current.toString());
            results.push(...pageResults);
            if (pageResults.length < constants_2.REPOSITORY_HOST_PROVIDER_PAGE_LIMIT) {
                return results;
            }
            page += 1;
        }
    }
    async requestJson(method, url, body) {
        return (0, api_support_1.requestGitLabJson)({
            method,
            url,
            token: this.token,
            errorName: GITLAB_REPOSITORY_HOST_ERROR_NAME,
            body,
        });
    }
}
exports.GitLabRepositoryHostClient = GitLabRepositoryHostClient;
function filterAssignableGitLabReviewers(input) {
    const assignableReviewerKeys = new Set([
        ...input.existingEligibleReviewers,
        ...input.resolvedReviewers.map((reviewer) => reviewer.login),
    ].map((login) => login.toLowerCase()));
    return input.eligibleReviewers.filter((reviewer) => assignableReviewerKeys.has(reviewer.toLowerCase()));
}
function mapMergeRequest(mergeRequest) {
    return {
        number: mergeRequest.iid,
        url: mergeRequest.web_url,
        state: mergeRequest.state === "merged"
            ? constants_1.PLATFORM_PULL_REQUEST_STATES.merged
            : mergeRequest.state === "opened"
                ? constants_1.PLATFORM_PULL_REQUEST_STATES.open
                : constants_1.PLATFORM_PULL_REQUEST_STATES.closed,
        isDraft: isDraftMergeRequest(mergeRequest),
        headBranch: mergeRequest.source_branch,
        baseBranch: mergeRequest.target_branch,
        headRefOid: mergeRequest.sha ?? null,
        mergeCommitOid: mergeRequest.merge_commit_sha ?? mergeRequest.squash_commit_sha ?? null,
        ...((mergeRequest.merged_at ?? mergeRequest.closed_at)
            ? { completedAt: mergeRequest.merged_at ?? mergeRequest.closed_at }
            : {}),
    };
}
function resolveDiffStatus(diff) {
    if (diff.deleted_file === true) {
        return "removed";
    }
    if (diff.new_file === true) {
        return "added";
    }
    if (diff.renamed_file === true) {
        return "renamed";
    }
    return "modified";
}
function firstLine(value) {
    return value.split(/\r?\n/, 1)[0]?.trim() ?? "";
}
function isDraftMergeRequest(mergeRequest) {
    return mergeRequest.draft === true || mergeRequest.work_in_progress === true;
}
function isOpenMergeRequest(mergeRequest) {
    return mergeRequest.state === "opened";
}
function resolveReviewDecision(mergeRequest, approvals) {
    if (resolveGitLabMergeStatus(mergeRequest) === "requested_changes") {
        return "CHANGES_REQUESTED";
    }
    if (approvals === null) {
        return "REVIEW_REQUIRED";
    }
    if ((approvals.approvals_left ?? 1) <= 0) {
        return "APPROVED";
    }
    return "REVIEW_REQUIRED";
}
function resolveMergeability(mergeRequest) {
    const status = resolveGitLabMergeStatus(mergeRequest);
    if (status === "mergeable" || status === "can_be_merged") {
        return "MERGEABLE";
    }
    if (status === "conflict" || status === "cannot_be_merged") {
        return "CONFLICTING";
    }
    return "UNKNOWN";
}
function resolveMergeStateStatus(mergeRequest) {
    if (isDraftMergeRequest(mergeRequest)) {
        return "DRAFT";
    }
    const status = resolveGitLabMergeStatus(mergeRequest);
    if (status === "mergeable" || status === "can_be_merged") {
        return "CLEAN";
    }
    if (status === "conflict" || status === "cannot_be_merged") {
        return "DIRTY";
    }
    if (status === "need_rebase") {
        return "BEHIND";
    }
    if (GITLAB_BLOCKED_MERGE_STATUSES.has(status)) {
        return "BLOCKED";
    }
    return "UNKNOWN";
}
function resolveGitLabMergeStatus(mergeRequest) {
    return mergeRequest.detailed_merge_status ?? mergeRequest.merge_status ?? "";
}
const GITLAB_BLOCKED_MERGE_STATUSES = new Set([
    "requested_changes",
    "discussions_not_resolved",
    "status_checks_must_pass",
    "security_policy_violations",
    "merge_request_blocked",
    "ci_must_pass",
    "not_approved",
    "title_regex",
    "jira_association_missing",
    "external_status_checks",
]);
function isRetryableGitLabMergeError(error) {
    if (!(0, api_support_1.isGitLabApiError)(error)) {
        return false;
    }
    return (error.status === 409 ||
        error.status === 429 ||
        error.status >= 500 ||
        /try again later|temporar|timeout|another operation|locked/iu.test(error.message));
}
