import { randomUUID } from "node:crypto";
import { Router } from "express";
import { and, count as countFn, eq } from "drizzle-orm";
import { agents as agentsTable } from "@paperclipai/db";
import { DEFAULT_FEEDBACK_DATA_SHARING_TERMS_VERSION, companyArtifactsQuerySchema, companyPortabilityExportSchema, companyPortabilityImportSchema, companyPortabilityPreviewSchema, createCompanySchema, feedbackTargetTypeSchema, feedbackTraceStatusSchema, feedbackVoteValueSchema, updateCompanyBrandingSchema, updateCompanySchema, } from "@paperclipai/shared";
import { badRequest, forbidden } from "../errors.js";
import { validate } from "../middleware/validate.js";
import { accessService, agentService, budgetService, companyArtifactsService, companyPortabilityService, companyService, feedbackService, logActivity, } from "../services/index.js";
import { assertBoard, assertCompanyAccess, assertInstanceAdmin, getActorInfo } from "./authz.js";
import { COMPANY_IMPORT_ROUTE_PATH } from "./company-import-paths.js";
export function companyRoutes(db, storage) {
    const router = Router();
    const svc = companyService(db);
    const agents = agentService(db);
    const portability = companyPortabilityService(db, storage);
    const access = accessService(db);
    const budgets = budgetService(db);
    const artifacts = companyArtifactsService(db, storage);
    const feedback = feedbackService(db);
    const importJobs = new Map();
    const importJobTerminalRetentionMs = 5 * 60 * 1000;
    function parseBooleanQuery(value) {
        return value === true || value === "true" || value === "1";
    }
    function parseDateQuery(value, field) {
        if (typeof value !== "string" || value.trim().length === 0)
            return undefined;
        const parsed = new Date(value);
        if (Number.isNaN(parsed.getTime())) {
            throw badRequest(`Invalid ${field} query value`);
        }
        return parsed;
    }
    function assertImportTargetAccess(req, target) {
        if (target.mode === "new_company") {
            assertInstanceAdmin(req);
            return;
        }
        assertCompanyAccess(req, target.companyId);
    }
    async function assertCanUpdateBranding(req, companyId) {
        assertCompanyAccess(req, companyId);
        if (req.actor.type === "board")
            return;
        if (!req.actor.agentId)
            throw forbidden("Agent authentication required");
        const actorAgent = await agents.getById(req.actor.agentId);
        if (!actorAgent || actorAgent.companyId !== companyId) {
            throw forbidden("Agent key cannot access another company");
        }
        if (actorAgent.role !== "ceo") {
            throw forbidden("Only CEO agents can update company branding");
        }
    }
    async function assertCanManagePortability(req, companyId, capability) {
        assertCompanyAccess(req, companyId);
        if (req.actor.type === "board")
            return;
        if (!req.actor.agentId)
            throw forbidden("Agent authentication required");
        const actorAgent = await agents.getById(req.actor.agentId);
        if (!actorAgent || actorAgent.companyId !== companyId) {
            throw forbidden("Agent key cannot access another company");
        }
        if (actorAgent.role !== "ceo") {
            throw forbidden(`Only CEO agents can manage company ${capability}`);
        }
    }
    router.get("/", async (req, res) => {
        assertBoard(req);
        const result = await svc.list();
        if (req.actor.source === "local_implicit" || req.actor.isInstanceAdmin) {
            res.json(result);
            return;
        }
        const allowed = new Set(req.actor.companyIds ?? []);
        res.json(result.filter((company) => allowed.has(company.id)));
    });
    router.get("/stats", async (req, res) => {
        assertBoard(req);
        const allowed = req.actor.source === "local_implicit" || req.actor.isInstanceAdmin
            ? null
            : new Set(req.actor.companyIds ?? []);
        const stats = await svc.stats();
        if (!allowed) {
            res.json(stats);
            return;
        }
        const filtered = Object.fromEntries(Object.entries(stats).filter(([companyId]) => allowed.has(companyId)));
        res.json(filtered);
    });
    // Common malformed path when companyId is empty in "/api/companies/{companyId}/issues".
    router.get("/issues", (_req, res) => {
        res.status(400).json({
            error: "Missing companyId in path. Use /api/companies/{companyId}/issues.",
        });
    });
    router.get("/:companyId/artifacts", async (req, res) => {
        const companyId = req.params.companyId;
        assertCompanyAccess(req, companyId);
        const query = companyArtifactsQuerySchema.parse(req.query);
        res.json(await artifacts.list(companyId, query));
    });
    router.get("/:companyId", async (req, res) => {
        const companyId = req.params.companyId;
        assertCompanyAccess(req, companyId);
        // Allow agents (CEO) to read their own company; board always allowed
        if (req.actor.type !== "agent") {
            assertBoard(req);
        }
        const company = await svc.getById(companyId);
        if (!company) {
            res.status(404).json({ error: "Company not found" });
            return;
        }
        res.json(company);
    });
    router.get("/:companyId/feedback-traces", async (req, res) => {
        const companyId = req.params.companyId;
        assertCompanyAccess(req, companyId);
        assertBoard(req);
        const targetTypeRaw = typeof req.query.targetType === "string" ? req.query.targetType : undefined;
        const voteRaw = typeof req.query.vote === "string" ? req.query.vote : undefined;
        const statusRaw = typeof req.query.status === "string" ? req.query.status : undefined;
        const issueId = typeof req.query.issueId === "string" && req.query.issueId.trim().length > 0 ? req.query.issueId : undefined;
        const projectId = typeof req.query.projectId === "string" && req.query.projectId.trim().length > 0
            ? req.query.projectId
            : undefined;
        const traces = await feedback.listFeedbackTraces({
            companyId,
            issueId,
            projectId,
            targetType: targetTypeRaw ? feedbackTargetTypeSchema.parse(targetTypeRaw) : undefined,
            vote: voteRaw ? feedbackVoteValueSchema.parse(voteRaw) : undefined,
            status: statusRaw ? feedbackTraceStatusSchema.parse(statusRaw) : undefined,
            from: parseDateQuery(req.query.from, "from"),
            to: parseDateQuery(req.query.to, "to"),
            sharedOnly: parseBooleanQuery(req.query.sharedOnly),
            includePayload: parseBooleanQuery(req.query.includePayload),
        });
        res.json(traces);
    });
    router.post("/:companyId/export", validate(companyPortabilityExportSchema), async (req, res) => {
        const companyId = req.params.companyId;
        await assertCanManagePortability(req, companyId, "exports");
        const result = await portability.exportBundle(companyId, req.body);
        res.json(result);
    });
    router.post("/import/preview", validate(companyPortabilityPreviewSchema), async (req, res) => {
        assertBoard(req);
        assertImportTargetAccess(req, req.body.target);
        const preview = await portability.previewImport(req.body);
        res.json(preview);
    });
    router.get("/import/jobs/:jobId", async (req, res) => {
        assertCloudTenantCaller(req);
        cleanupTerminalImportJobs(importJobs, importJobTerminalRetentionMs);
        const job = importJobs.get(req.params.jobId);
        if (!job || job.cloudTenantKey !== cloudTenantRequestKey(req)) {
            res.status(404).json({ error: "Import job not found" });
            return;
        }
        res.json(importJobResponse(job));
    });
    router.post(COMPANY_IMPORT_ROUTE_PATH, async (req, res) => {
        assertBoard(req);
        const rawImportBody = req.body;
        const actor = getActorInfo(req);
        const boardUserId = req.actor.type === "board" ? req.actor.userId : null;
        if (req.header("x-paperclip-cloud-async-import") === "1") {
            assertCloudTenantCaller(req);
            cleanupTerminalImportJobs(importJobs, importJobTerminalRetentionMs);
            const job = createImportJob(cloudTenantRequestKey(req));
            importJobs.set(job.id, job);
            const operation = async () => {
                const importBody = companyPortabilityImportSchema.parse(rawImportBody);
                assertImportTargetAccess(req, importBody.target);
                const activity = importedCompanyActivityContext(actor, importBody.include ?? null);
                const result = await portability.importBundle(importBody, boardUserId);
                await logImportedCompanyActivity(db, activity, result);
                return result;
            };
            res.status(202).json(importJobAcceptedResponse(job));
            setImmediate(() => {
                void runImportJob(job, operation);
            });
            return;
        }
        const importBody = companyPortabilityImportSchema.parse(rawImportBody);
        assertImportTargetAccess(req, importBody.target);
        const activity = importedCompanyActivityContext(actor, importBody.include ?? null);
        const result = await portability.importBundle(importBody, boardUserId);
        await logImportedCompanyActivity(db, activity, result);
        res.json(result);
    });
    router.post("/:companyId/exports/preview", validate(companyPortabilityExportSchema), async (req, res) => {
        const companyId = req.params.companyId;
        await assertCanManagePortability(req, companyId, "exports");
        const preview = await portability.previewExport(companyId, req.body);
        res.json(preview);
    });
    router.post("/:companyId/exports", validate(companyPortabilityExportSchema), async (req, res) => {
        const companyId = req.params.companyId;
        await assertCanManagePortability(req, companyId, "exports");
        const result = await portability.exportBundle(companyId, req.body);
        res.json(result);
    });
    router.post("/:companyId/imports/preview", validate(companyPortabilityPreviewSchema), async (req, res) => {
        const companyId = req.params.companyId;
        await assertCanManagePortability(req, companyId, "imports");
        if (req.body.target.mode === "existing_company" && req.body.target.companyId !== companyId) {
            throw forbidden("Safe import route can only target the route company");
        }
        if (req.body.collisionStrategy === "replace") {
            throw forbidden("Safe import route does not allow replace collision strategy");
        }
        const preview = await portability.previewImport(req.body, {
            mode: "agent_safe",
            sourceCompanyId: companyId,
        });
        res.json(preview);
    });
    router.post("/:companyId/imports/apply", validate(companyPortabilityImportSchema), async (req, res) => {
        const companyId = req.params.companyId;
        await assertCanManagePortability(req, companyId, "imports");
        if (req.body.target.mode === "existing_company" && req.body.target.companyId !== companyId) {
            throw forbidden("Safe import route can only target the route company");
        }
        if (req.body.collisionStrategy === "replace") {
            throw forbidden("Safe import route does not allow replace collision strategy");
        }
        const actor = getActorInfo(req);
        const result = await portability.importBundle(req.body, req.actor.type === "board" ? req.actor.userId : null, {
            mode: "agent_safe",
            sourceCompanyId: companyId,
        });
        await logActivity(db, {
            companyId: result.company.id,
            actorType: actor.actorType,
            actorId: actor.actorId,
            entityType: "company",
            entityId: result.company.id,
            agentId: actor.agentId,
            runId: actor.runId,
            action: "company.imported",
            details: {
                include: req.body.include ?? null,
                agentCount: result.agents.length,
                warningCount: result.warnings.length,
                companyAction: result.company.action,
                importMode: "agent_safe",
            },
        });
        res.json(result);
    });
    router.post("/", validate(createCompanySchema), async (req, res) => {
        assertBoard(req);
        if (!(req.actor.source === "local_implicit" || req.actor.isInstanceAdmin)) {
            throw forbidden("Instance admin required");
        }
        const company = await svc.create(req.body);
        const ownerPrincipalId = req.actor.userId ?? "local-board";
        await access.ensureMembership(company.id, "user", ownerPrincipalId, "owner", "active");
        await access.ensureRoleDefaultGrants(company.id, ownerPrincipalId, "owner", req.actor.userId ?? null);
        await logActivity(db, {
            companyId: company.id,
            actorType: "user",
            actorId: req.actor.userId ?? "board",
            action: "company.created",
            entityType: "company",
            entityId: company.id,
            details: { name: company.name },
        });
        if (company.budgetMonthlyCents > 0) {
            await budgets.upsertPolicy(company.id, {
                scopeType: "company",
                scopeId: company.id,
                amount: company.budgetMonthlyCents,
                windowKind: "calendar_month_utc",
            }, req.actor.userId ?? "board");
        }
        res.status(201).json(company);
    });
    router.patch("/:companyId", async (req, res) => {
        const companyId = req.params.companyId;
        assertCompanyAccess(req, companyId);
        const actor = getActorInfo(req);
        const existingCompany = await svc.getById(companyId);
        if (!existingCompany) {
            res.status(404).json({ error: "Company not found" });
            return;
        }
        let body;
        if (req.actor.type === "agent") {
            // Only CEO agents may update company branding fields
            const agentSvc = agentService(db);
            const actorAgent = req.actor.agentId ? await agentSvc.getById(req.actor.agentId) : null;
            if (!actorAgent || actorAgent.role !== "ceo") {
                throw forbidden("Only CEO agents or board users may update company settings");
            }
            if (actorAgent.companyId !== companyId) {
                throw forbidden("Agent key cannot access another company");
            }
            body = updateCompanyBrandingSchema.parse(req.body);
        }
        else {
            assertBoard(req);
            body = updateCompanySchema.parse(req.body);
            if (body.feedbackDataSharingEnabled === true && !existingCompany.feedbackDataSharingEnabled) {
                body = {
                    ...body,
                    feedbackDataSharingConsentAt: new Date(),
                    feedbackDataSharingConsentByUserId: req.actor.userId ?? "local-board",
                    feedbackDataSharingTermsVersion: typeof body.feedbackDataSharingTermsVersion === "string" && body.feedbackDataSharingTermsVersion.length > 0
                        ? body.feedbackDataSharingTermsVersion
                        : DEFAULT_FEEDBACK_DATA_SHARING_TERMS_VERSION,
                };
            }
        }
        const transitionsToArchived = body.status === "archived" && existingCompany.status !== "archived";
        const transitionsArchivedToActive = body.status === "active" && existingCompany.status === "archived";
        let transitionsPausedToActiveWithArchivePausedAgents = false;
        if (body.status === "active" && existingCompany.status === "paused") {
            const [archivedPausedCount] = await db
                .select({ value: countFn() })
                .from(agentsTable)
                .where(and(eq(agentsTable.companyId, companyId), eq(agentsTable.status, "paused"), eq(agentsTable.pauseReason, "company_archived")));
            transitionsPausedToActiveWithArchivePausedAgents =
                Number(archivedPausedCount?.value ?? 0) > 0;
        }
        const lifecycleEventEmittedByService = transitionsToArchived ||
            transitionsArchivedToActive ||
            transitionsPausedToActiveWithArchivePausedAgents;
        const company = await svc.update(companyId, body, actor);
        if (!company) {
            res.status(404).json({ error: "Company not found" });
            return;
        }
        if (!lifecycleEventEmittedByService) {
            await logActivity(db, {
                companyId,
                actorType: actor.actorType,
                actorId: actor.actorId,
                agentId: actor.agentId,
                runId: actor.runId,
                action: "company.updated",
                entityType: "company",
                entityId: companyId,
                details: body,
            });
        }
        res.json(company);
    });
    router.patch("/:companyId/branding", validate(updateCompanyBrandingSchema), async (req, res) => {
        const companyId = req.params.companyId;
        await assertCanUpdateBranding(req, companyId);
        const company = await svc.update(companyId, req.body);
        if (!company) {
            res.status(404).json({ error: "Company not found" });
            return;
        }
        const actor = getActorInfo(req);
        await logActivity(db, {
            companyId,
            actorType: actor.actorType,
            actorId: actor.actorId,
            agentId: actor.agentId,
            runId: actor.runId,
            action: "company.branding_updated",
            entityType: "company",
            entityId: companyId,
            details: req.body,
        });
        res.json(company);
    });
    router.post("/:companyId/archive", async (req, res) => {
        assertBoard(req);
        const companyId = req.params.companyId;
        assertCompanyAccess(req, companyId);
        const company = await svc.archive(companyId, getActorInfo(req));
        if (!company) {
            res.status(404).json({ error: "Company not found" });
            return;
        }
        res.json(company);
    });
    router.delete("/:companyId", async (req, res) => {
        assertBoard(req);
        const companyId = req.params.companyId;
        assertCompanyAccess(req, companyId);
        const company = await svc.remove(companyId);
        if (!company) {
            res.status(404).json({ error: "Company not found" });
            return;
        }
        res.json({ ok: true });
    });
    return router;
}
function assertCloudTenantCaller(req) {
    if (req.actor.source !== "cloud_tenant") {
        throw forbidden("Trusted Cloud tenant access required");
    }
}
function cloudTenantRequestKey(req) {
    return [
        req.actor.userId ?? "",
        req.header("x-paperclip-cloud-stack-id")?.trim() ?? "",
        req.header("x-paperclip-cloud-paperclip-company-id")?.trim() ?? "",
    ].join(":");
}
function createImportJob(cloudTenantKey) {
    const now = new Date().toISOString();
    return {
        id: `tenant-import-${randomUUID()}`,
        cloudTenantKey,
        status: "running",
        createdAt: now,
        updatedAt: now,
    };
}
async function runImportJob(job, operation) {
    try {
        const result = await operation();
        const now = new Date().toISOString();
        job.status = "succeeded";
        job.updatedAt = now;
        job.completedAt = now;
        job.result = {
            companyId: result.company.id,
            agentCount: result.agents.length,
            warningCount: result.warnings.length,
            companyAction: result.company.action,
        };
    }
    catch (error) {
        const now = new Date().toISOString();
        job.status = "failed";
        job.updatedAt = now;
        job.completedAt = now;
        job.error = { message: errorMessage(error) };
    }
}
function importedCompanyActivityContext(actor, include) {
    return {
        actorType: actor.actorType,
        actorId: actor.actorId,
        agentId: actor.agentId,
        runId: actor.runId,
        include,
    };
}
async function logImportedCompanyActivity(db, activity, result) {
    await logActivity(db, {
        companyId: result.company.id,
        actorType: activity.actorType,
        actorId: activity.actorId,
        action: "company.imported",
        entityType: "company",
        entityId: result.company.id,
        agentId: activity.agentId,
        runId: activity.runId,
        details: {
            include: activity.include,
            agentCount: result.agents.length,
            warningCount: result.warnings.length,
            companyAction: result.company.action,
        },
    });
}
function importJobAcceptedResponse(job) {
    return {
        job: {
            id: job.id,
            status: job.status,
        },
        statusUrl: `/api/companies/import/jobs/${encodeURIComponent(job.id)}`,
        retryAfterMs: 1000,
    };
}
function importJobResponse(job) {
    const isTerminal = job.status === "succeeded" || job.status === "failed";
    const response = {
        job: {
            id: job.id,
            status: job.status,
            createdAt: job.createdAt,
            updatedAt: job.updatedAt,
            ...(job.completedAt ? { completedAt: job.completedAt } : {}),
            ...(job.error ? { error: job.error } : {}),
            ...(job.result ? { result: job.result } : {}),
        },
        ...(isTerminal ? {} : { retryAfterMs: 1000 }),
    };
    if (job.error?.message) {
        response.error = job.error.message;
        response.message = job.error.message;
        response.reason = job.error.message;
    }
    return response;
}
function cleanupTerminalImportJobs(importJobs, terminalRetentionMs) {
    const now = Date.now();
    for (const [jobId, job] of importJobs) {
        if (job.status === "running" || !job.completedAt)
            continue;
        if (now - Date.parse(job.completedAt) > terminalRetentionMs) {
            importJobs.delete(jobId);
        }
    }
}
function errorMessage(error) {
    return error instanceof Error && error.message.trim() ? error.message : String(error);
}
//# sourceMappingURL=companies.js.map