"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.runStateCommand = runStateCommand;
const control_plane_config_1 = require("../config/control-plane-config");
const service_state_1 = require("../core/service-state");
const factory_1 = require("../platform/auth/factory");
const provider_resolution_1 = require("../platform/provider-resolution");
const factory_2 = require("../platform/ticket-tracker/factory");
const scoped_ticket_tracker_client_1 = require("../platform/ticket-tracker/scoped-ticket-tracker-client");
const state_patch_1 = require("./state-patch");
const state_backup_1 = require("./state-backup");
const state_canonical_comment_1 = require("./state-canonical-comment");
const catalog_1 = require("../i18n/catalog");
const prompts_1 = require("./prompts");
async function runStateCommand(cwd, options, deps = {}) {
    switch (options.action) {
        case "patch":
            return runStatePatchCommand(cwd, options, deps);
        case "restore":
            return runStateRestoreCommand(cwd, options, deps);
    }
}
async function runStatePatchCommand(cwd, options, deps) {
    if (!(await ensureServiceStopped(deps))) {
        return 1;
    }
    const clients = await loadStateCommandClients(cwd, deps);
    const issues = await loadStatePatchTargetIssues(clients.ticketTracker, options);
    const locale = clients.config.i18n.defaultLanguage;
    if (issues.length > 0 &&
        !options.dryRun &&
        !(await (0, prompts_1.confirmIfInteractive)(deps.prompter ?? (0, prompts_1.createCliPrompter)({ locale }), {
            label: (0, catalog_1.translate)(locale, "state.command.patch.confirm", {
                count: issues.length,
            }),
            onCanceled: () => console.log((0, catalog_1.translate)(locale, "state.command.canceled")),
        }))) {
        return 1;
    }
    const result = await (0, state_patch_1.patchCanonicalWorkflowStates)({
        cwd,
        config: clients.config,
        ticketTracker: clients.ticketTracker,
        issues,
        dryRun: options.dryRun,
        now: (deps.now ?? (() => new Date()))(),
        ...(deps.homeDirectory ? { homeDirectory: deps.homeDirectory } : {}),
        deps,
    });
    for (const { issue, outcome } of result.outcomes) {
        recordPatchOutcome(issue, outcome);
    }
    console.log((0, state_patch_1.formatStatePatchSummary)(result.summary));
    if (result.summary.updated > 0 && !options.dryRun) {
        console.log(`backup: ${result.backupFilePath}`);
    }
    return result.summary.future > 0 ||
        result.summary.skipped > 0 ||
        result.summary.failed > 0
        ? 1
        : 0;
}
async function runStateRestoreCommand(cwd, options, deps) {
    if (!(await ensureServiceStopped(deps))) {
        return 1;
    }
    const clients = await loadStateCommandClients(cwd, deps);
    const records = filterBackupRecordsByIssueIds(await (0, state_backup_1.readStateRestorableBackupRecords)(options.backupFilePath), options.issueIds);
    const locale = clients.config.i18n.defaultLanguage;
    if (records.length > 0 &&
        !options.dryRun &&
        !(await (0, prompts_1.confirmIfInteractive)(deps.prompter ?? (0, prompts_1.createCliPrompter)({ locale }), {
            label: (0, catalog_1.translate)(locale, "state.command.restore.confirm", {
                count: countUniqueBackupIssues(records),
            }),
            onCanceled: () => console.log((0, catalog_1.translate)(locale, "state.command.canceled")),
        }))) {
        return 1;
    }
    const now = (deps.now ?? (() => new Date()))();
    const runId = (0, state_backup_1.createStatePatchRunId)(now);
    const restoreBackupFilePath = (0, state_backup_1.resolveStateRestoreBackupFilePath)(options.backupFilePath);
    const summary = createCommandSummary();
    let sequence = 0;
    if (records.length === 0) {
        summary.noop += 1;
    }
    for (const record of records) {
        const outcome = await restoreIssueCanonicalState({
            record,
            options,
            clients,
            runId,
            restoreBackupFilePath,
            now,
            nextSequence: () => {
                sequence += 1;
                return sequence;
            },
        });
        recordRestoreOutcome(summary, record, outcome);
    }
    console.log(`state restore summary: restored=${summary.restored} noop=${summary.noop} skipped=${summary.skipped} failed=${summary.failed}`);
    if (summary.restored > 0 && !options.dryRun) {
        console.log(`restore backup: ${restoreBackupFilePath}`);
    }
    return summary.skipped > 0 || summary.failed > 0 ? 1 : 0;
}
function countUniqueBackupIssues(records) {
    return new Set(records.map((record) => record.issue.id)).size;
}
async function restoreIssueCanonicalState(input) {
    try {
        input.clients.ticketTracker.resetVolatileIssueReads(input.record.issue.id);
        const comments = await input.clients.ticketTracker.listComments(input.record.issue.id, { timeline: "issue-only" });
        const targetComment = comments.find((comment) => comment.id === input.record.comment.id);
        if (!targetComment) {
            return { kind: "skipped", reason: "target comment missing" };
        }
        const latest = (0, state_canonical_comment_1.resolveLatestCanonicalComment)(comments);
        if (latest.kind !== "found" || latest.comment.id !== targetComment.id) {
            return {
                kind: "skipped",
                reason: "newer canonical workflow state comment exists",
            };
        }
        const currentHash = (0, state_backup_1.sha256Hex)(targetComment.body);
        if (!input.options.force &&
            currentHash !== input.record.hashes.patchedBodySha256) {
            return {
                kind: "skipped",
                reason: "canonical workflow state comment does not match backup",
            };
        }
        if (targetComment.body === input.record.originalBody) {
            return { kind: "noop" };
        }
        if (input.options.dryRun) {
            return { kind: "restored" };
        }
        await (0, state_backup_1.appendStateBackupRecord)(input.restoreBackupFilePath, {
            type: "restore_comment_backup",
            schemaVersion: 1,
            runId: input.runId,
            sequence: input.nextSequence(),
            createdAt: input.now.toISOString(),
            sourceBackupFilePath: input.options.backupFilePath,
            issue: input.record.issue,
            comment: {
                id: targetComment.id,
                url: targetComment.url,
                updatedAt: targetComment.updatedAt ?? null,
            },
            hashes: {
                currentBodySha256: currentHash,
                restoredBodySha256: (0, state_backup_1.sha256Hex)(input.record.originalBody),
            },
            currentBody: targetComment.body,
            restoredBody: input.record.originalBody,
        });
        const precondition = await (0, state_canonical_comment_1.verifyLatestCanonicalCommentUnchanged)({
            ticketTracker: input.clients.ticketTracker,
            issueId: input.record.issue.id,
            expectedCommentId: targetComment.id,
            expectedBodySha256: currentHash,
            changedReason: "canonical workflow state comment changed before restore",
        });
        if (precondition.kind === "skipped") {
            return precondition;
        }
        await input.clients.ticketTracker.updateComment(precondition.comment.id, input.record.originalBody);
        return { kind: "restored" };
    }
    catch (error) {
        return { kind: "failed", reason: formatError(error) };
    }
}
async function loadStateCommandClients(cwd, deps) {
    const { config } = (0, provider_resolution_1.resolveSupportedProviders)(await (deps.loadConfig ?? control_plane_config_1.loadConfig)(cwd));
    const authProvider = (deps.createAuthProvider ?? factory_1.createAuthProvider)(config);
    const rawTicketTracker = await (deps.createTicketTrackerClient ?? factory_2.createTicketTrackerClient)(config, authProvider);
    const ticketTracker = (deps.createScopedTicketTrackerClient ?? scoped_ticket_tracker_client_1.createScopedTicketTrackerClient)(rawTicketTracker);
    return { config, ticketTracker };
}
async function ensureServiceStopped(deps) {
    const liveService = await (deps.getLiveServiceState ?? service_state_1.getLiveServiceState)(deps.homeDirectory);
    if (!liveService) {
        return true;
    }
    console.error(`Service is running (pid ${liveService.pid}); stop it first.`);
    return false;
}
async function loadStatePatchTargetIssues(ticketTracker, options) {
    if (options.issueIds !== undefined && options.issueIds.length > 0) {
        return Promise.all([...new Set(options.issueIds)].map((issueId) => ticketTracker.getIssueRecord(issueId)));
    }
    return ticketTracker.listIssues({
        state: options.state ?? "open",
        includeSourceBranchName: false,
    });
}
function filterBackupRecordsByIssueIds(records, issueIds) {
    if (issueIds === undefined || issueIds.length === 0) {
        return [...records];
    }
    const targetIssueIds = new Set(issueIds);
    return records.filter((record) => targetIssueIds.has(record.issue.id) ||
        targetIssueIds.has(record.issue.displayId) ||
        targetIssueIds.has(record.issue.displayId.replace(/^#/u, "")));
}
function createCommandSummary() {
    return {
        updated: 0,
        noop: 0,
        skipped: 0,
        failed: 0,
        restored: 0,
    };
}
function recordPatchOutcome(issue, outcome) {
    if (outcome.kind === "future" ||
        outcome.kind === "skipped" ||
        outcome.kind === "failed") {
        console.error((0, state_patch_1.formatStatePatchOutcomeForIssue)(issue, outcome));
    }
}
function recordRestoreOutcome(summary, record, outcome) {
    summary[outcome.kind] += 1;
    if (outcome.kind === "skipped" || outcome.kind === "failed") {
        console.error(`${record.issue.displayId} ${outcome.kind}: ${outcome.reason}`);
    }
}
function formatError(error) {
    return error instanceof Error ? error.message : String(error);
}
