"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GiteaRepositoryHostClient = void 0;
const constants_1 = require("../constants");
const auth_1 = require("../gitea/auth");
const project_ref_1 = require("../gitea/project-ref");
const constants_2 = require("./constants");
const errors_1 = require("./errors");
const login_support_1 = require("./login-support");
const GITEA_ALREADY_MERGED_MESSAGE = /\balready(?:\s+been)?\s+merged\b/i;
const GITEA_RETRYABLE_MERGE_MESSAGE = /please try again later/i;
class GiteaRepositoryHostClient {
    project;
    authorizationHeader;
    constructor(target, credential) {
        this.project = (0, project_ref_1.parseGiteaProjectRef)(target.projectRef);
        this.authorizationHeader = (0, auth_1.toGiteaAuthorizationHeader)(credential);
    }
    async getRepository() {
        const repository = await this.requestJson(`/repos/${this.project.owner}/${this.project.repo}`);
        return { defaultBranch: repository.default_branch };
    }
    async findPullRequestByBranch(branchName) {
        for (let page = 1;; page += 1) {
            const pulls = await this.requestJsonArray(`/repos/${this.project.owner}/${this.project.repo}/pulls`, {
                state: "all",
                page: String(page),
                limit: String(constants_2.REPOSITORY_HOST_PROVIDER_PAGE_LIMIT),
            });
            for (const pull of pulls) {
                if (pull.head?.ref === branchName) {
                    return mapPullRequest(pull);
                }
            }
            if (pulls.length < constants_2.REPOSITORY_HOST_PROVIDER_PAGE_LIMIT) {
                return null;
            }
        }
    }
    async getPullRequest(pullRequestNumber) {
        try {
            return mapPullRequest(await this.getPullRequestDetails(pullRequestNumber));
        }
        catch {
            return null;
        }
    }
    async getPullRequestAutomationState(pullRequestNumber) {
        const pull = await this.requestJson(`/repos/${this.project.owner}/${this.project.repo}/pulls/${pullRequestNumber}`);
        if (!isOpenPullRequest(pull)) {
            return null;
        }
        const reviews = await this.requestJsonArray(`/repos/${this.project.owner}/${this.project.repo}/pulls/${pullRequestNumber}/reviews`);
        const reviewDecision = deriveReviewDecision(reviews);
        const latestApprovedReviewCommitOid = reviewDecision === "APPROVED"
            ? resolveLatestApprovedReviewCommitOid(reviews)
            : null;
        const result = {
            number: pull.number,
            url: resolvePullRequestUrl(pull),
            reviewDecision,
            mergeability: mapMergeability(pull),
            isDraft: Boolean(pull.draft),
            headRefOid: pull.head_sha ?? pull.head?.sha ?? "",
            latestApprovedReviewCommitOid,
        };
        result.mergeStateStatus = mapMergeStateStatus(pull);
        return result;
    }
    async createPullRequest(input) {
        const pull = await this.requestJson(`/repos/${this.project.owner}/${this.project.repo}/pulls`, undefined, {
            title: input.title,
            body: input.body,
            head: input.headBranch,
            base: input.baseBranch,
        }, "POST");
        return {
            ...mapPullRequest(pull),
            isDraft: false,
        };
    }
    async listPullRequestChangedFiles(pullRequestNumber) {
        const files = await this.requestJsonArray(`/repos/${this.project.owner}/${this.project.repo}/pulls/${pullRequestNumber}/files`);
        return files.flatMap((file) => {
            const path = file.filename ?? file.name ?? "";
            if (!path) {
                return [];
            }
            return [
                {
                    path,
                    status: file.status ?? null,
                    additions: file.additions ?? null,
                    deletions: file.deletions ?? null,
                    changes: file.changes ?? null,
                },
            ];
        });
    }
    async listPullRequestCommits(pullRequestNumber) {
        const commits = await this.requestJsonArray(`/repos/${this.project.owner}/${this.project.repo}/pulls/${pullRequestNumber}/commits`);
        return commits.flatMap((commit) => {
            const oid = commit.sha ?? commit.id ?? "";
            if (!oid) {
                return [];
            }
            return [
                {
                    oid,
                    title: firstLine(commit.commit?.message ?? commit.message ?? ""),
                    authorName: commit.commit?.author?.name ??
                        commit.author?.login ??
                        commit.author?.username ??
                        null,
                    authoredAt: commit.commit?.author?.date ?? null,
                    url: commit.html_url ?? commit.url ?? null,
                },
            ];
        });
    }
    async mergePullRequest(pullRequestNumber, method) {
        try {
            await this.requestJson(`/repos/${this.project.owner}/${this.project.repo}/pulls/${pullRequestNumber}/merge`, undefined, {
                Do: mapMergeMethod(method),
            }, "POST");
        }
        catch (error) {
            if (isAlreadyMergedMergeError(error)) {
                return;
            }
            if (await this.isPullRequestMerged(pullRequestNumber)) {
                return;
            }
            if (isRetryableMergeError(error)) {
                throw new errors_1.RetryablePullRequestMergeError(error instanceof Error ? error.message : String(error));
            }
            throw error;
        }
    }
    async isPullRequestHeadIncludedInBase(pullRequestNumber) {
        const pull = await this.getPullRequestDetails(pullRequestNumber);
        const baseBranch = pull.base?.ref ?? "";
        const headSha = pull.head_sha ?? pull.head?.sha ?? "";
        if (!baseBranch || !headSha) {
            return false;
        }
        const comparison = await this.requestJson(`/repos/${this.project.owner}/${this.project.repo}/compare/${encodeURIComponent(`${baseBranch}...${headSha}`)}`);
        return (comparison.total_commits ?? comparison.commits?.length ?? 0) === 0;
    }
    async syncPullRequestAssignees(pullRequestNumber, assignees) {
        const normalizedAssignees = (0, login_support_1.normalizeLogins)(assignees);
        if (normalizedAssignees.length === 0) {
            return;
        }
        const pull = await this.getPullRequestDetails(pullRequestNumber);
        const existingAssignees = (0, login_support_1.normalizeLogins)((pull.assignees ?? []).flatMap((assignee) => assignee.login ? [assignee.login] : []));
        const assigneesToAdd = (0, login_support_1.findMissingLogins)({
            requestedLogins: normalizedAssignees,
            existingLogins: existingAssignees,
        });
        if (assigneesToAdd.length === 0) {
            return;
        }
        await this.requestJson(`/repos/${this.project.owner}/${this.project.repo}/issues/${pullRequestNumber}`, undefined, {
            assignees: [...existingAssignees, ...assigneesToAdd],
        }, "PATCH");
    }
    async syncPullRequestReviewers(pullRequestNumber, reviewers) {
        const normalizedReviewers = (0, login_support_1.normalizeLogins)(reviewers);
        if (normalizedReviewers.length === 0) {
            return { eligibleReviewers: [] };
        }
        const pull = await this.getPullRequestDetails(pullRequestNumber);
        const eligibleReviewers = (0, login_support_1.findMissingLogins)({
            requestedLogins: normalizedReviewers,
            existingLogins: [],
            excludedLogins: [pull.user?.login],
        });
        const reviewersToRequest = (0, login_support_1.findMissingLogins)({
            requestedLogins: eligibleReviewers,
            existingLogins: (pull.requested_reviewers ?? []).map((reviewer) => reviewer.login),
        });
        if (reviewersToRequest.length === 0) {
            return { eligibleReviewers };
        }
        await this.requestJson(`/repos/${this.project.owner}/${this.project.repo}/pulls/${pullRequestNumber}/requested_reviewers`, undefined, {
            reviewers: reviewersToRequest,
        }, "POST");
        return { eligibleReviewers };
    }
    async markPullRequestReadyForReview(_pullRequestNumber) { }
    async getPullRequestDetails(pullRequestNumber) {
        return this.requestJson(`/repos/${this.project.owner}/${this.project.repo}/pulls/${pullRequestNumber}`);
    }
    async isPullRequestMerged(pullRequestNumber) {
        try {
            return isMergedPullRequest(await this.getPullRequestDetails(pullRequestNumber));
        }
        catch {
            return false;
        }
    }
    async requestJson(apiPath, searchParams, body, method = "GET") {
        const init = {
            method,
            headers: {
                accept: "application/json",
                authorization: this.authorizationHeader,
                ...(body === undefined
                    ? {}
                    : {
                        "content-type": "application/json",
                    }),
            },
        };
        if (body !== undefined) {
            init.body = JSON.stringify(body);
        }
        const response = await fetch(this.buildApiUrl(apiPath, searchParams), init);
        if (!response.ok) {
            throw new Error(await readGiteaError(response));
        }
        if (response.status === 204) {
            return undefined;
        }
        const text = await response.text();
        if (text.trim().length === 0) {
            return undefined;
        }
        return JSON.parse(text);
    }
    async requestJsonArray(apiPath, searchParams) {
        const response = await fetch(this.buildApiUrl(apiPath, searchParams), {
            headers: {
                accept: "application/json",
                authorization: this.authorizationHeader,
            },
        });
        if (!response.ok) {
            throw new Error(await readGiteaError(response));
        }
        const payload = (await response.json());
        if (Array.isArray(payload)) {
            return payload;
        }
        if (payload &&
            typeof payload === "object" &&
            Array.isArray(payload.data)) {
            return payload.data;
        }
        return [];
    }
    buildApiUrl(apiPath, searchParams) {
        const url = new URL((0, project_ref_1.buildGiteaApiUrl)(`${this.project.origin}/${this.project.owner}/${this.project.repo}`, apiPath));
        if (searchParams) {
            for (const [key, value] of Object.entries(searchParams)) {
                url.searchParams.set(key, value);
            }
        }
        return url.toString();
    }
}
exports.GiteaRepositoryHostClient = GiteaRepositoryHostClient;
function mapPullRequest(pullRequest) {
    return {
        number: pullRequest.number,
        url: resolvePullRequestUrl(pullRequest),
        state: isMergedPullRequest(pullRequest)
            ? constants_1.PLATFORM_PULL_REQUEST_STATES.merged
            : isOpenPullRequest(pullRequest)
                ? constants_1.PLATFORM_PULL_REQUEST_STATES.open
                : constants_1.PLATFORM_PULL_REQUEST_STATES.closed,
        isDraft: Boolean(pullRequest.draft),
        headBranch: pullRequest.head?.ref ?? "",
        baseBranch: pullRequest.base?.ref ?? "",
        headRefOid: pullRequest.head_sha ?? pullRequest.head?.sha ?? null,
        mergeCommitOid: pullRequest.merge_commit_sha ?? pullRequest.merge_commit_id ?? null,
        ...((pullRequest.merged_at ?? pullRequest.closed_at)
            ? { completedAt: pullRequest.merged_at ?? pullRequest.closed_at }
            : {}),
    };
}
function resolvePullRequestUrl(pullRequest) {
    return pullRequest.html_url ?? pullRequest.url ?? "";
}
function isOpenPullRequest(pullRequest) {
    if (isMergedPullRequest(pullRequest)) {
        return false;
    }
    return pullRequest.state === "open";
}
function isMergedPullRequest(pullRequest) {
    return Boolean(pullRequest.merged_at ?? pullRequest.merged);
}
function isAlreadyMergedMergeError(error) {
    const message = error instanceof Error ? error.message : String(error);
    return GITEA_ALREADY_MERGED_MESSAGE.test(message);
}
function isRetryableMergeError(error) {
    const message = error instanceof Error ? error.message : String(error);
    return GITEA_RETRYABLE_MERGE_MESSAGE.test(message);
}
function deriveReviewDecision(reviews) {
    const latestStateByLogin = new Map();
    for (const review of sortReviews(reviews)) {
        const login = review.user?.login?.toLowerCase();
        if (!login) {
            continue;
        }
        latestStateByLogin.set(login, review);
    }
    const latestReviews = [...latestStateByLogin.values()];
    if (latestReviews.some((review) => isChangesRequestedReviewState(review.state ?? ""))) {
        return "CHANGES_REQUESTED";
    }
    if (latestReviews.some((review) => isApprovedReviewState(review.state ?? ""))) {
        return "APPROVED";
    }
    return "REVIEW_REQUIRED";
}
function resolveLatestApprovedReviewCommitOid(reviews) {
    return (sortReviews(reviews)
        .filter((review) => isApprovedReviewState(review.state ?? ""))
        .at(-1)?.commit_id ?? null);
}
function sortReviews(reviews) {
    return [...reviews].sort((left, right) => {
        const leftSubmittedAt = left.submitted_at ?? "";
        const rightSubmittedAt = right.submitted_at ?? "";
        const submittedAtComparison = leftSubmittedAt.localeCompare(rightSubmittedAt);
        if (submittedAtComparison !== 0) {
            return submittedAtComparison;
        }
        return (left.id ?? 0) - (right.id ?? 0);
    });
}
function isApprovedReviewState(state) {
    return state.toUpperCase() === "APPROVED";
}
function isChangesRequestedReviewState(state) {
    const normalized = state.toUpperCase().replace(/[^A-Z]/g, "_");
    return (normalized === "CHANGES_REQUESTED" ||
        normalized === "REQUEST_CHANGES" ||
        normalized === "REQUESTED_CHANGES" ||
        (normalized.includes("CHANGE") && normalized.includes("REQUEST")));
}
function mapMergeability(pullRequest) {
    const mergeable = pullRequest.mergeable;
    if (typeof mergeable === "boolean") {
        return mergeable ? "MERGEABLE" : "CONFLICTING";
    }
    if (typeof mergeable === "string") {
        const normalized = mergeable.toLowerCase();
        if (normalized.includes("merge")) {
            return "MERGEABLE";
        }
        if (normalized.includes("conflict") || normalized.includes("dirty")) {
            return "CONFLICTING";
        }
    }
    return "UNKNOWN";
}
function mapMergeStateStatus(pullRequest) {
    const mergeableState = pullRequest.merge_state_status ?? pullRequest.mergeable_state;
    if (typeof mergeableState === "string") {
        const normalized = mergeableState.toLowerCase();
        switch (normalized) {
            case "clean":
                return "CLEAN";
            case "dirty":
            case "conflict":
            case "conflicting":
                return "DIRTY";
            case "behind":
                return "BEHIND";
            case "blocked":
                return "BLOCKED";
            case "draft":
                return "DRAFT";
            case "has_hooks":
            case "has-hooks":
                return "HAS_HOOKS";
            case "unstable":
                return "UNSTABLE";
            case "unknown":
                return "UNKNOWN";
            default:
                return "UNKNOWN";
        }
    }
    if (typeof pullRequest.mergeable === "boolean") {
        return pullRequest.mergeable ? "CLEAN" : "DIRTY";
    }
    return "UNKNOWN";
}
function firstLine(value) {
    return value.split(/\r?\n/, 1)[0]?.trim() ?? "";
}
async function readGiteaError(response) {
    const defaultMessage = `Gitea request failed with status ${response.status}.`;
    const text = await response.text().catch(() => "");
    if (text.trim().length === 0) {
        return defaultMessage;
    }
    try {
        const body = JSON.parse(text);
        if (typeof body.message === "string" && body.message.trim().length > 0) {
            return body.message;
        }
    }
    catch {
        return text;
    }
    return text;
}
function mapMergeMethod(method) {
    switch (method) {
        case "squash":
            return "squash";
    }
}
