var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
import { inject, injectable } from 'tsyringe';
import path from 'node:path';
import { CloudDeploymentStatus, OperationLogKind, OperationLogLevel, } from '../../../domain/generated/output.js';
import { ApplicationNotFoundError } from '../../../domain/errors/application-not-found.error.js';
import { NoProviderSelectedError } from '../../../domain/errors/no-provider-selected.error.js';
import { BuildOutputNotFoundError } from '../../../domain/errors/build-output-not-found.error.js';
import { CloudProviderNotConnectedError } from '../../../domain/errors/cloud-provider-not-connected.error.js';
import { ProviderNotImplementedError } from '../../../domain/errors/provider-not-implemented.error.js';
const BUILD_OUTPUT_CANDIDATES = ['dist', 'build', '.next', 'out'];
let InitiateCloudDeploymentUseCase = class InitiateCloudDeploymentUseCase {
    applicationRepo;
    registry;
    fs;
    eventBus;
    logger;
    opLog;
    buildService;
    constructor(applicationRepo, registry, fs, eventBus, logger, opLog, buildService) {
        this.applicationRepo = applicationRepo;
        this.registry = registry;
        this.fs = fs;
        this.eventBus = eventBus;
        this.logger = logger;
        this.opLog = opLog;
        this.buildService = buildService;
    }
    async execute(input) {
        const opId = input.applicationId;
        const opKind = OperationLogKind.CloudDeploy;
        const app = await this.applicationRepo.findById(input.applicationId);
        if (!app) {
            await this.opLog.error(opKind, opId, `Application not found: ${input.applicationId}`);
            throw new ApplicationNotFoundError(input.applicationId);
        }
        // Note: we deliberately do NOT gate on `app.setupComplete` here.
        // That flag was originally a guard against deploying a half-
        // scaffolded brand-new app whose initial AI-generated build hadn't
        // finished yet, but it has two failure modes that hurt real users:
        //   1. Legacy apps created before setup_complete was a column
        //      have it stuck on `false` forever.
        //   2. Apps where the orchestrator was killed mid-scaffold (a dev
        //      restart, a crash, a manual stop) end up with the flag stuck
        //      on `false` even though every meaningful artifact is in place
        //      — the user has been editing, the repo has commits, etc.
        // The real precondition for a deploy is "does a build output dir
        // exist?", which `resolveBuildOutputDir()` checks below and throws
        // `BuildOutputNotFoundError` for. That's a more accurate, more
        // actionable error than this paternalistic flag check, so we let
        // it be the gatekeeper instead.
        // Provider resolution order:
        //   1. Explicit input.provider from the caller — wins unconditionally
        //   2. The per-application `app.cloudDeploymentProvider` — set by the
        //      SelectCloudProvider use case when the user picks one from the UI
        //   3. Fallback: pick the first enabled + connected provider from the
        //      registry. This makes the happy path "click Deploy without first
        //      fiddling with a provider picker" work — if the user has exactly
        //      one connected provider there's no ambiguity, and if they have
        //      multiple the first-wins tiebreak is predictable.
        //
        // Earlier code threw NoProviderSelectedError on step 2 missing, which
        // left users staring at an error for an action that should just work.
        let providerId = input.provider ?? app.cloudDeploymentProvider;
        if (!providerId) {
            const firstConnected = await this.findFirstConnectedProvider();
            if (firstConnected) {
                providerId = firstConnected;
                // Persist the auto-pick onto the Application row so subsequent
                // deploys from any entry point (CLI, web, script) see the same
                // selection the user implicitly agreed to on this click.
                await this.applicationRepo.update(input.applicationId, {
                    cloudDeploymentProvider: providerId,
                });
                await this.opLog.info(opKind, opId, `Auto-selected provider ${providerId} (first connected)`);
            }
        }
        if (!providerId) {
            await this.opLog.error(opKind, opId, 'No cloud provider connected — connect one from the Deploy panel first');
            throw new NoProviderSelectedError(input.applicationId);
        }
        await this.opLog.info(opKind, opId, `Starting deploy to ${providerId}`);
        const provider = this.registry.get(providerId);
        if (!provider.enabled) {
            await this.opLog.error(opKind, opId, `Provider ${providerId} is not enabled in this build`);
            throw new ProviderNotImplementedError(providerId);
        }
        if (!(await provider.isConnected())) {
            await this.opLog.error(opKind, opId, `Provider ${providerId} reports not connected (token missing or invalid)`);
            throw new CloudProviderNotConnectedError(providerId);
        }
        // Re-run the project's build script so the deploy always ships fresh output.
        await this.opLog.info(opKind, opId, `Building project before deploy…`);
        try {
            await this.buildService.buildProject(app.repositoryPath, (line) => {
                void this.opLog.debug(opKind, opId, line);
            });
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            await this.opLog.error(opKind, opId, `Build failed: ${msg}`);
            throw err;
        }
        const buildOutputDir = this.resolveBuildOutputDir(app.repositoryPath);
        await this.opLog.info(opKind, opId, `Resolved build output directory: ${buildOutputDir}`);
        // Record initial state and emit.
        await this.applicationRepo.update(input.applicationId, {
            cloudDeploymentProvider: providerId,
            cloudDeploymentStatus: CloudDeploymentStatus.Uploading,
            cloudDeploymentError: undefined,
            lastDeployedAt: new Date(),
        });
        this.publish(input.applicationId, providerId, CloudDeploymentStatus.Uploading);
        try {
            const result = await provider.deploy({
                applicationId: input.applicationId,
                buildOutputDir,
                // Use `app.slug` (which already carries a 6-char random hex
                // suffix for uniqueness) as the cloud project name. This is
                // critical: without the suffix, deploying an app named e.g.
                // "Landing Page" would silently OVERWRITE any pre-existing
                // Cloudflare Pages project of the same name in the user's
                // account — catastrophic when the user has an unrelated
                // project with that name. The suffix makes collisions
                // astronomically unlikely while still letting subsequent
                // deploys of the same Shep application reuse the same cloud
                // project (same slug → same project).
                projectName: app.slug,
            }, (status, message) => {
                void this.applicationRepo
                    .update(input.applicationId, { cloudDeploymentStatus: status })
                    .catch((errr) => this.logger.warn('failed to persist interim deploy status', { err: String(errr) }));
                this.publish(input.applicationId, providerId, status, undefined, undefined, message);
                // Persist the lifecycle transition as a user-visible log entry too
                // — `void` because we never want a logging hiccup to abort a deploy.
                void this.opLog.info(opKind, opId, message ?? `Status transitioned to ${status}`);
            }, 
            // Provider-internal log lines flow through this callback. The provider
            // never touches the log store directly — the use case is the only
            // thing that calls IOperationLogService.
            (level, message, detail) => {
                void this.appendInternalLog(opKind, opId, level, message, detail);
            });
            await this.applicationRepo.update(input.applicationId, {
                cloudDeploymentStatus: CloudDeploymentStatus.Deployed,
                cloudDeploymentId: result.deploymentId,
                cloudDeploymentUrl: result.url,
                cloudDeploymentError: undefined,
                lastDeployedAt: new Date(),
            });
            this.publish(input.applicationId, providerId, CloudDeploymentStatus.Deployed, result.url);
            await this.opLog.info(opKind, opId, `Deploy succeeded — live at ${result.url}`);
            return result;
        }
        catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            await this.applicationRepo.update(input.applicationId, {
                cloudDeploymentStatus: CloudDeploymentStatus.Failed,
                cloudDeploymentError: message,
                lastDeployedAt: new Date(),
            });
            this.publish(input.applicationId, providerId, CloudDeploymentStatus.Failed, undefined, message);
            await this.opLog.error(opKind, opId, `Deploy failed: ${message}`, err instanceof Error && err.stack ? err.stack : undefined);
            throw err;
        }
    }
    /**
     * Single funnel for provider-emitted log lines — translates the
     * level enum into the right opLog method. Kept private so callers
     * can't accidentally bypass the level→method mapping.
     */
    appendInternalLog(kind, id, level, message, detail) {
        switch (level) {
            case OperationLogLevel.Debug:
                return this.opLog.debug(kind, id, message, detail);
            case OperationLogLevel.Info:
                return this.opLog.info(kind, id, message, detail);
            case OperationLogLevel.Warn:
                return this.opLog.warn(kind, id, message, detail);
            case OperationLogLevel.Error:
                return this.opLog.error(kind, id, message, detail);
            default:
                return this.opLog.info(kind, id, message, detail);
        }
    }
    /**
     * Walk the provider registry in declared order and return the first
     * provider that is both `enabled` AND `isConnected()`. Used to pick
     * a sensible default when a deploy is initiated on an application
     * that has no explicit `cloudDeploymentProvider` yet.
     *
     * Returns `null` if nothing is connected — the caller then throws
     * NoProviderSelectedError with a friendlier "connect something first"
     * message.
     */
    async findFirstConnectedProvider() {
        const descriptors = this.registry.listAll();
        for (const descriptor of descriptors) {
            if (!descriptor.enabled)
                continue;
            const provider = this.registry.get(descriptor.id);
            try {
                if (await provider.isConnected())
                    return descriptor.id;
            }
            catch {
                // isConnected() can throw on transient token-verify failures.
                // Treat as "not connected" and keep searching.
            }
        }
        return null;
    }
    resolveBuildOutputDir(repositoryPath) {
        for (const candidate of BUILD_OUTPUT_CANDIDATES) {
            const full = path.join(repositoryPath, candidate);
            if (this.fs.pathExists(full))
                return full;
        }
        throw new BuildOutputNotFoundError(BUILD_OUTPUT_CANDIDATES.map((c) => path.join(repositoryPath, c)));
    }
    publish(applicationId, provider, status, url, error, message) {
        this.eventBus.publish({
            applicationId,
            provider,
            status,
            url,
            error,
            message,
            timestamp: Date.now(),
        });
    }
};
InitiateCloudDeploymentUseCase = __decorate([
    injectable(),
    __param(0, inject('IApplicationRepository')),
    __param(1, inject('ICloudDeploymentProviderRegistry')),
    __param(2, inject('IFileSystemService')),
    __param(3, inject('ICloudDeploymentEventBus')),
    __param(4, inject('ILogger')),
    __param(5, inject('IOperationLogService')),
    __param(6, inject('IProjectBuildService')),
    __metadata("design:paramtypes", [Object, Object, Object, Object, Object, Object, Object])
], InitiateCloudDeploymentUseCase);
export { InitiateCloudDeploymentUseCase };
