import { randomBytes } from 'node:crypto';
import path from 'node:path';

import { TRPCError } from '@trpc/server';
import { z } from 'zod';

import { resolvePostgresConfigFromEnv } from '@sammo-ts/infra';

import { procedure, router } from './trpc.js';
import { listScenarioPreviews, resolveGitCommitSha } from './scenario/scenarioCatalog.js';
import type { UserSanctions, UserServerRestriction } from './auth/userRepository.js';
import { toPublicUser } from './auth/userRepository.js';
import type { AdminAuthContext } from './adminAuth.js';
import type { GatewayApiContext } from './context.js';
import { GATEWAY_BUILD_STATUSES, GATEWAY_PROFILE_STATUSES } from './orchestrator/profileRepository.js';
import { seedProfileDatabase } from './orchestrator/seedProfileDatabase.js';

const zProfileStatus = z.enum(GATEWAY_PROFILE_STATUSES);
const zBuildStatus = z.enum(GATEWAY_BUILD_STATUSES);
const zUserRoleMode = z.enum(['set', 'grant', 'revoke']);
const zJoinMode = z.enum(['full', 'onlyRandom']);
const zServerAction = z.enum([
    'RESUME',
    'PAUSE',
    'STOP',
    'ACCELERATE',
    'DELAY',
    'RESET_NOW',
    'RESET_SCHEDULED',
    'OPEN_SURVEY',
    'SHUTDOWN',
]);

const TURN_TERM_MINUTES = [1, 2, 5, 10, 20, 30, 60, 120] as const;
const AUTORUN_USER_OPTIONS = [
    'develop',
    'warp',
    'recruit',
    'recruit_high',
    'train',
    'battle',
    'chief',
] as const;

const ADMIN_ROLE_PREFIX = 'admin.';
const ADMIN_ROLE_SUPERUSER = 'admin.superuser';
const ROLE_SUPERUSER = 'superuser';
const ROLE_ADMIN_USERS = 'admin.users.manage';
const ROLE_ADMIN_USERS_CREATE = 'admin.users.create';
const ROLE_ADMIN_PROFILES = 'admin.profiles.manage';
const ROLE_ADMIN_NOTICE = 'admin.notice.manage';
const ROLE_RESET_SCHEDULE = 'admin.reset.schedule';
const ROLE_RESUME_WHEN_STOPPED = 'admin.resume.when-stopped';
const ROLE_SURVEY_OPEN = 'admin.survey.open';

const readSessionToken = (headers: Record<string, string | string[] | undefined>): string | null => {
    const provided = headers['x-session-token'] ?? headers['authorization'] ?? '';
    const raw = Array.isArray(provided) ? (provided[0] ?? '') : (provided as string);
    const token = raw.startsWith('Bearer ') ? raw.slice(7) : raw;
    const trimmed = token.trim();
    return trimmed ? trimmed : null;
};

const isFirstUser = async (ctx: GatewayApiContext, userId: string): Promise<boolean> => {
    const first = await ctx.prisma.appUser.findFirst({
        orderBy: { createdAt: 'asc' },
        select: { id: true },
    });
    return first?.id === userId;
};

const resolveAdminAuth = async (ctx: GatewayApiContext): Promise<AdminAuthContext> => {
    const token = readSessionToken(ctx.requestHeaders);
    if (!token) {
        throw new TRPCError({
            code: 'UNAUTHORIZED',
            message: 'Session token is required.',
        });
    }
    const session = await ctx.sessions.getSession(token);
    if (!session) {
        throw new TRPCError({
            code: 'UNAUTHORIZED',
            message: 'Session is not valid.',
        });
    }
    const user = await ctx.users.findById(session.userId);
    if (!user) {
        throw new TRPCError({
            code: 'UNAUTHORIZED',
            message: 'User not found.',
        });
    }
    const roles = user.roles;
    const isSuperuser =
        roles.includes(ROLE_SUPERUSER) ||
        roles.includes(ADMIN_ROLE_SUPERUSER) ||
        (await isFirstUser(ctx, session.userId));
    const hasAdminRole = isSuperuser || roles.some((role) => role === 'admin' || role.startsWith(ADMIN_ROLE_PREFIX));
    if (!hasAdminRole) {
        throw new TRPCError({
            code: 'FORBIDDEN',
            message: 'Admin permission is required.',
        });
    }
    return {
        session,
        user,
        roles,
        isSuperuser,
    };
};

const requireAdminAuth = (ctx: { adminAuth?: AdminAuthContext }): AdminAuthContext => {
    if (!ctx.adminAuth) {
        throw new TRPCError({
            code: 'UNAUTHORIZED',
            message: 'Admin session is not available.',
        });
    }
    return ctx.adminAuth;
};

const roleMatchesScope = (role: string, permission: string, profileName?: string): boolean => {
    if (role === permission || role === `${permission}:*`) {
        return true;
    }
    if (profileName && role === `${permission}:${profileName}`) {
        return true;
    }
    return false;
};

const hasScopedPermission = (adminAuth: AdminAuthContext, permission: string, profileName?: string): boolean => {
    if (adminAuth.isSuperuser) {
        return true;
    }
    return adminAuth.roles.some((role: string) => roleMatchesScope(role, permission, profileName));
};

const assertPermission = (adminAuth: AdminAuthContext, permission: string, profileName?: string): void => {
    if (hasScopedPermission(adminAuth, permission, profileName)) {
        return;
    }
    throw new TRPCError({
        code: 'FORBIDDEN',
        message: 'Permission denied.',
    });
};

const canCreateLocalUser = (adminAuth: AdminAuthContext): boolean =>
    hasScopedPermission(adminAuth, ROLE_ADMIN_USERS_CREATE) || hasScopedPermission(adminAuth, ROLE_ADMIN_USERS);

// 로컬 계정 임의 생성은 환경 설정이 켜져 있을 때만 허용한다.
const assertLocalAccountEnabled = (ctx: GatewayApiContext): void => {
    if (ctx.adminLocalAccountEnabled) {
        return;
    }
    throw new TRPCError({
        code: 'FORBIDDEN',
        message: 'Local account provisioning is disabled.',
    });
};

const adminProcedure = procedure.use(async ({ ctx, next }) => {
    const adminAuth = await resolveAdminAuth(ctx as GatewayApiContext);
    return next({
        ctx: {
            ...ctx,
            adminAuth,
        },
    });
});

const noticeAdminProcedure = adminProcedure.use(({ ctx, next }) => {
    const adminAuth = requireAdminAuth(ctx);
    assertPermission(adminAuth, ROLE_ADMIN_NOTICE);
    return next();
});

const userAdminProcedure = adminProcedure.use(({ ctx, next }) => {
    const adminAuth = requireAdminAuth(ctx);
    assertPermission(adminAuth, ROLE_ADMIN_USERS);
    return next();
});

const userCreateProcedure = adminProcedure.use(({ ctx, next }) => {
    const adminAuth = requireAdminAuth(ctx);
    if (!canCreateLocalUser(adminAuth)) {
        throw new TRPCError({
            code: 'FORBIDDEN',
            message: 'Permission denied.',
        });
    }
    return next();
});

const profileAdminProcedure = adminProcedure.use(({ ctx, next }) => {
    const adminAuth = requireAdminAuth(ctx);
    assertPermission(adminAuth, ROLE_ADMIN_PROFILES);
    return next();
});

const zUserLookupInput = z
    .object({
        id: z.string().min(1).optional(),
        username: z.string().min(1).optional(),
        email: z.string().min(3).optional(),
    })
    .refine((value) => Boolean(value.id || value.username || value.email), {
        message: 'id, username, or email must be provided.',
    });

const zServerRestriction = z.object({
    blockedFeatures: z.array(z.string().min(1)).optional(),
    until: z.string().datetime().nullable().optional(),
    reason: z.string().max(200).nullable().optional(),
    notes: z.string().max(2000).nullable().optional(),
});

const zSanctionsPatch = z.object({
    bannedUntil: z.string().datetime().nullable().optional(),
    mutedUntil: z.string().datetime().nullable().optional(),
    suspendedUntil: z.string().datetime().nullable().optional(),
    warningCount: z.number().int().min(0).nullable().optional(),
    flags: z.array(z.string().min(1)).nullable().optional(),
    notes: z.string().max(2000).nullable().optional(),
    profileIconResetAt: z.string().datetime().nullable().optional(),
    serverRestrictions: z.record(z.string(), zServerRestriction.nullable()).nullable().optional(),
});

const zLocalAccountInput = z.object({
    username: z.string().min(2).max(32),
    password: z.string().min(6).max(128),
    displayName: z.string().min(2).max(40).optional(),
});

const zInstallAutorun = z.object({
    limitMinutes: z.number().int().min(0).max(43200),
    options: z.array(z.enum(AUTORUN_USER_OPTIONS)),
});

const isAllowedTurnTerm = (value: number): boolean => TURN_TERM_MINUTES.some((term) => term === value);

const zInstallOptions = z.object({
    scenarioId: z.number().int().min(0),
    turnTermMinutes: z.number().int().refine((value) => isAllowedTurnTerm(value), {
        message: 'turnTermMinutes must divide 120.',
    }),
    sync: z.boolean(),
    fiction: z.number().int().min(0).max(1),
    extend: z.boolean(),
    blockGeneralCreate: z.number().int().min(0).max(2),
    npcMode: z.number().int().min(0).max(2),
    showImgLevel: z.number().int().min(0).max(3),
    tournamentTrig: z.boolean(),
    joinMode: zJoinMode,
    autorunUser: zInstallAutorun.nullable().optional(),
    openAt: z.string().datetime().optional(),
    preopenAt: z.string().datetime().optional(),
    gitRef: z.string().min(1).max(128).optional(),
});

type SanctionsPatch = z.infer<typeof zSanctionsPatch>;

// 제재 패치 입력을 현재 제재 상태에 병합한다.
const applySanctionsPatch = (current: UserSanctions, patch: SanctionsPatch): UserSanctions => {
    const next: UserSanctions = { ...current };
    const applyField = <K extends keyof UserSanctions>(key: K, value: UserSanctions[K] | null | undefined): void => {
        if (value === undefined) {
            return;
        }
        if (value === null) {
            delete next[key];
            return;
        }
        next[key] = value;
    };

    applyField('bannedUntil', patch.bannedUntil);
    applyField('mutedUntil', patch.mutedUntil);
    applyField('suspendedUntil', patch.suspendedUntil);
    applyField('warningCount', patch.warningCount);
    applyField('flags', patch.flags);
    applyField('notes', patch.notes);
    applyField('profileIconResetAt', patch.profileIconResetAt);

    if (patch.serverRestrictions !== undefined) {
        if (patch.serverRestrictions === null) {
            delete next.serverRestrictions;
        } else {
            const existing = { ...(next.serverRestrictions ?? {}) };
            for (const [profile, restriction] of Object.entries(patch.serverRestrictions)) {
                if (!restriction) {
                    delete existing[profile];
                } else {
                    const merged: UserServerRestriction = {
                        ...(existing[profile] ?? {}),
                    };
                    if (restriction.blockedFeatures !== undefined) {
                        merged.blockedFeatures = restriction.blockedFeatures ?? undefined;
                    }
                    if (restriction.until !== undefined) {
                        merged.until = restriction.until ?? undefined;
                    }
                    if (restriction.reason !== undefined) {
                        merged.reason = restriction.reason ?? undefined;
                    }
                    if (restriction.notes !== undefined) {
                        merged.notes = restriction.notes ?? undefined;
                    }
                    existing[profile] = merged;
                }
            }
            next.serverRestrictions = existing;
        }
    }

    return next;
};

const buildAdminPassword = (): string => randomBytes(6).toString('hex');

// 프로필 메타를 안전하게 읽고, 패치를 병합한다.
const readMetaObject = (value: unknown): Record<string, unknown> => {
    if (!value || typeof value !== 'object') {
        return {};
    }
    return value as Record<string, unknown>;
};

const applyMetaPatch = (
    meta: Record<string, unknown>,
    patch: Record<string, unknown | null | undefined>
): Record<string, unknown> => {
    const next = { ...meta };
    for (const [key, value] of Object.entries(patch)) {
        if (value === undefined) {
            continue;
        }
        if (value === null) {
            delete next[key];
            continue;
        }
        next[key] = value;
    }
    return next;
};

export const adminRouter = router({
    system: router({
        getNotice: adminProcedure.query(async ({ ctx }) => {
            const setting = await ctx.prisma.systemSetting.findUnique({
                where: { id: 1 },
            });
            return { notice: setting?.notice ?? '' };
        }),
        setNotice: noticeAdminProcedure
            .input(
                z.object({
                    notice: z.string().max(4000),
                })
            )
            .mutation(async ({ ctx, input }) => {
                const setting = await ctx.prisma.systemSetting.upsert({
                    where: { id: 1 },
                    create: {
                        id: 1,
                        notice: input.notice,
                    },
                    update: {
                        notice: input.notice,
                    },
                });
                return { notice: setting.notice };
            }),
    }),
    users: router({
        getLocalAccountStatus: adminProcedure.query(({ ctx }) => ({
            enabled: (ctx as GatewayApiContext).adminLocalAccountEnabled,
        })),
        createLocal: userCreateProcedure.input(zLocalAccountInput).mutation(async ({ ctx, input }) => {
            const gatewayCtx = ctx as GatewayApiContext;
            assertLocalAccountEnabled(gatewayCtx);
            const existing = await gatewayCtx.users.findByUsername(input.username);
            if (existing) {
                throw new TRPCError({
                    code: 'CONFLICT',
                    message: 'Username already exists.',
                });
            }
            try {
                const created = await gatewayCtx.users.createUser({
                    username: input.username,
                    password: input.password,
                    displayName: input.displayName,
                });
                return { user: toPublicUser(created) };
            } catch (error) {
                throw new TRPCError({
                    code: 'CONFLICT',
                    message: 'Username already exists.',
                    cause: error,
                });
            }
        }),
        lookup: userAdminProcedure.input(zUserLookupInput).query(async ({ ctx, input }) => {
            const user = input.id
                ? await ctx.users.findById(input.id)
                : input.username
                  ? await ctx.users.findByUsername(input.username)
                  : input.email
                    ? await ctx.users.findByEmail(input.email)
                    : null;
            if (!user) {
                return null;
            }
            return {
                id: user.id,
                username: user.username,
                displayName: user.displayName,
                roles: user.roles,
                sanctions: user.sanctions,
                oauthType: user.oauthType,
                oauthId: user.oauthId,
                email: user.email,
                createdAt: user.createdAt,
            };
        }),
        resetPassword: userAdminProcedure
            .input(
                z.object({
                    userId: z.string().min(1),
                    newPassword: z.string().min(6).max(128).optional(),
                })
            )
            .mutation(async ({ ctx, input }) => {
                const password = input.newPassword ?? buildAdminPassword();
                await ctx.users.updatePassword(input.userId, password);
                await ctx.flushPublisher.publishUserFlush(input.userId, 'admin-password-reset');
                return { password };
            }),
        updateRoles: userAdminProcedure
            .input(
                z.object({
                    userId: z.string().min(1),
                    roles: z.array(z.string().min(1)).min(1),
                    mode: zUserRoleMode.optional(),
                })
            )
            .mutation(async ({ ctx, input }) => {
                const user = await ctx.users.findById(input.userId);
                if (!user) {
                    throw new TRPCError({
                        code: 'NOT_FOUND',
                        message: 'User not found.',
                    });
                }
                const mode = input.mode ?? 'set';
                const roles = new Set(user.roles);
                if (mode === 'set') {
                    roles.clear();
                    for (const role of input.roles) {
                        roles.add(role);
                    }
                } else if (mode === 'grant') {
                    for (const role of input.roles) {
                        roles.add(role);
                    }
                } else {
                    for (const role of input.roles) {
                        roles.delete(role);
                    }
                }
                const nextRoles = Array.from(roles);
                await ctx.users.updateRoles(input.userId, nextRoles);
                await ctx.flushPublisher.publishUserFlush(input.userId, 'admin-roles-updated');
                return { roles: nextRoles };
            }),
        updateSanctions: userAdminProcedure
            .input(
                z.object({
                    userId: z.string().min(1),
                    patch: zSanctionsPatch,
                })
            )
            .mutation(async ({ ctx, input }) => {
                const user = await ctx.users.findById(input.userId);
                if (!user) {
                    throw new TRPCError({
                        code: 'NOT_FOUND',
                        message: 'User not found.',
                    });
                }
                const next = applySanctionsPatch(user.sanctions, input.patch);
                await ctx.users.updateSanctions(input.userId, next);
                await ctx.flushPublisher.publishUserFlush(input.userId, 'admin-sanctions-updated');
                return { sanctions: next };
            }),
        setServerRestriction: userAdminProcedure
            .input(
                z.object({
                    userId: z.string().min(1),
                    profile: z.string().min(1).max(64),
                    restriction: zServerRestriction.nullable(),
                })
            )
            .mutation(async ({ ctx, input }) => {
                const user = await ctx.users.findById(input.userId);
                if (!user) {
                    throw new TRPCError({
                        code: 'NOT_FOUND',
                        message: 'User not found.',
                    });
                }
                const patch: SanctionsPatch = {
                    serverRestrictions: {
                        [input.profile]: input.restriction ?? null,
                    },
                };
                const next = applySanctionsPatch(user.sanctions, patch);
                await ctx.users.updateSanctions(input.userId, next);
                await ctx.flushPublisher.publishUserFlush(input.userId, 'admin-server-restriction');
                return { sanctions: next };
            }),
        resetProfileIcon: userAdminProcedure
            .input(
                z.object({
                    userId: z.string().min(1),
                })
            )
            .mutation(async ({ ctx, input }) => {
                const user = await ctx.users.findById(input.userId);
                if (!user) {
                    throw new TRPCError({
                        code: 'NOT_FOUND',
                        message: 'User not found.',
                    });
                }
                const next = applySanctionsPatch(user.sanctions, {
                    profileIconResetAt: new Date().toISOString(),
                });
                await ctx.users.updateSanctions(input.userId, next);
                await ctx.flushPublisher.publishUserFlush(input.userId, 'admin-profile-icon-reset');
                return { profileIconResetAt: next.profileIconResetAt };
            }),
        forceDelete: userAdminProcedure
            .input(
                z.object({
                    userId: z.string().min(1),
                })
            )
            .mutation(async ({ ctx, input }) => {
                await ctx.flushPublisher.publishUserFlush(input.userId, 'admin-force-withdraw');
                await ctx.users.deleteUser(input.userId);
                return { ok: true };
            }),
    }),
    profiles: router({
        list: adminProcedure.query(async ({ ctx }) => {
            const profiles = await ctx.profiles.listProfiles();
            const runtimeStates = await ctx.orchestrator.listRuntimeStates(
                profiles.map((profile) => profile.profileName)
            );
            const runtimeMap = new Map(runtimeStates.map((state) => [state.profileName, state]));
            return profiles.map((profile) => ({
                ...profile,
                runtime: runtimeMap.get(profile.profileName) ?? {
                    profileName: profile.profileName,
                    apiRunning: false,
                    daemonRunning: false,
                },
            }));
        }),
        listScenarios: profileAdminProcedure
            .input(
                z
                    .object({
                        gitRef: z.string().min(1).max(128).optional(),
                    })
                    .optional()
            )
            .query(async ({ input }) => {
                const gitRef = input?.gitRef?.trim();
                return listScenarioPreviews({ gitRef: gitRef || null });
            }),
        upsert: profileAdminProcedure
            .input(
                z.object({
                    profile: z.string().min(1).max(32),
                    scenario: z.string().min(1).max(64),
                    apiPort: z.number().int().min(1).max(65535),
                    status: zProfileStatus.optional(),
                    preopenAt: z.string().datetime().optional(),
                    openAt: z.string().datetime().optional(),
                    scheduledStartAt: z.string().datetime().optional(),
                    buildCommitSha: z.string().min(7).max(64).optional(),
                })
            )
            .mutation(async ({ ctx, input }) => {
                const status = input.status ?? 'STOPPED';
                return ctx.profiles.upsertProfile({
                    profile: input.profile,
                    scenario: input.scenario,
                    apiPort: input.apiPort,
                    status,
                    preopenAt: input.preopenAt,
                    openAt: input.openAt,
                    scheduledStartAt: input.scheduledStartAt,
                    buildCommitSha: input.buildCommitSha,
                });
            }),
        setStatus: profileAdminProcedure
            .input(
                z.object({
                    profileName: z.string().min(1),
                    status: zProfileStatus,
                    preopenAt: z.string().datetime().optional(),
                    openAt: z.string().datetime().optional(),
                    scheduledStartAt: z.string().datetime().optional(),
                    buildCommitSha: z.string().min(7).max(64).optional(),
                })
            )
            .mutation(async ({ ctx, input }) => {
                if (input.status === 'RESERVED' && (!input.preopenAt || !input.openAt)) {
                    throw new TRPCError({
                        code: 'BAD_REQUEST',
                        message: 'preopenAt and openAt are required for RESERVED status.',
                    });
                }
                const result = await ctx.profiles.updateStatus(input.profileName, input.status, {
                    preopenAt: input.preopenAt,
                    openAt: input.openAt,
                    scheduledStartAt: input.status === 'RESERVED' ? input.scheduledStartAt : null,
                });
                if (input.buildCommitSha) {
                    await ctx.profiles.updateBuildStatus(input.profileName, 'IDLE', {
                        commitSha: input.buildCommitSha,
                    });
                }
                await ctx.orchestrator.reconcileNow();
                return result;
            }),
        updateMeta: profileAdminProcedure
            .input(
                z.object({
                    profileName: z.string().min(1),
                    patch: z.object({
                        korName: z.string().min(1).max(64).nullable().optional(),
                        color: z.string().min(1).max(32).nullable().optional(),
                        inGameNotice: z.string().max(4000).nullable().optional(),
                        profileImageUrl: z.string().max(2048).nullable().optional(),
                    }),
                })
            )
            .mutation(async ({ ctx, input }) => {
                const profile = await ctx.profiles.getProfile(input.profileName);
                if (!profile) {
                    throw new TRPCError({
                        code: 'NOT_FOUND',
                        message: 'Profile not found.',
                    });
                }
                const meta = readMetaObject(profile.meta);
                const nextMeta = applyMetaPatch(meta, input.patch);
                return ctx.profiles.updateMeta(input.profileName, nextMeta);
            }),
        install: profileAdminProcedure
            .input(
                z.object({
                    profileName: z.string().min(1),
                    install: zInstallOptions,
                    reason: z.string().max(200).optional(),
                })
            )
            .mutation(async ({ ctx, input }) => {
                const adminAuth = requireAdminAuth(ctx);
                const profile = await ctx.profiles.getProfile(input.profileName);
                if (!profile) {
                    throw new TRPCError({
                        code: 'NOT_FOUND',
                        message: 'Profile not found.',
                    });
                }

                const now = new Date();
                const openAt = input.install.openAt ? new Date(input.install.openAt) : null;
                const preopenAt = input.install.preopenAt ? new Date(input.install.preopenAt) : null;
                if (openAt && Number.isNaN(openAt.getTime())) {
                    throw new TRPCError({
                        code: 'BAD_REQUEST',
                        message: 'openAt is invalid.',
                    });
                }
                if (preopenAt && Number.isNaN(preopenAt.getTime())) {
                    throw new TRPCError({
                        code: 'BAD_REQUEST',
                        message: 'preopenAt is invalid.',
                    });
                }
                if (openAt && openAt.getTime() < now.getTime()) {
                    throw new TRPCError({
                        code: 'BAD_REQUEST',
                        message: 'openAt must be in the future.',
                    });
                }
                if (preopenAt && !openAt) {
                    throw new TRPCError({
                        code: 'BAD_REQUEST',
                        message: 'openAt is required when preopenAt is set.',
                    });
                }
                if (preopenAt && openAt && preopenAt.getTime() >= openAt.getTime()) {
                    throw new TRPCError({
                        code: 'BAD_REQUEST',
                        message: 'preopenAt must be earlier than openAt.',
                    });
                }

                const autorunUser = input.install.autorunUser ?? null;
                if (autorunUser) {
                    if (autorunUser.limitMinutes <= 0 && autorunUser.options.length > 0) {
                        throw new TRPCError({
                            code: 'BAD_REQUEST',
                            message: 'autorunUser limitMinutes must be positive when options are provided.',
                        });
                    }
                    if (autorunUser.limitMinutes > 0 && autorunUser.options.length === 0) {
                        throw new TRPCError({
                            code: 'BAD_REQUEST',
                            message: 'autorunUser options must be provided when limitMinutes is set.',
                        });
                    }
                }

                const scenarioValue = String(input.install.scenarioId);
                if (profile.scenario !== scenarioValue) {
                    try {
                        await ctx.profiles.updateScenario(profile.profileName, scenarioValue);
                    } catch (error) {
                        throw new TRPCError({
                            code: 'CONFLICT',
                            message: 'Scenario update failed due to duplication.',
                        });
                    }
                }

                let resolvedCommitSha: string | null = null;
                const gitRef = input.install.gitRef?.trim();
                if (gitRef) {
                    try {
                        resolvedCommitSha = await resolveGitCommitSha(gitRef);
                    } catch (error) {
                        throw new TRPCError({
                            code: 'BAD_REQUEST',
                            message: 'git ref is invalid or unavailable.',
                        });
                    }
                    await ctx.profiles.updateBuildStatus(profile.profileName, profile.buildStatus, {
                        commitSha: resolvedCommitSha,
                    });
                }

                const scheduledAt = openAt ? (preopenAt ?? openAt).toISOString() : null;
                const action = scheduledAt ? 'RESET_SCHEDULED' : 'RESET_NOW';
                const meta = readMetaObject(profile.meta);
                const actionLog = Array.isArray(meta.adminActions)
                    ? meta.adminActions.filter((entry) => entry && typeof entry === 'object')
                    : [];
                const actionRecord = {
                    action,
                    requestedAt: now.toISOString(),
                    scheduledAt,
                    reason: input.reason ?? null,
                    status: 'REQUESTED',
                    install: {
                        ...input.install,
                        openAt: input.install.openAt ?? null,
                        preopenAt: input.install.preopenAt ?? null,
                        gitRef: gitRef ?? null,
                        autorunUser: autorunUser
                            ? {
                                  limitMinutes: autorunUser.limitMinutes,
                                  options: autorunUser.options,
                              }
                            : null,
                        adminUser: {
                            id: adminAuth.user.id,
                            username: adminAuth.user.username,
                            displayName: adminAuth.user.displayName,
                        },
                    },
                };

                const nextMeta = {
                    ...meta,
                    adminActions: [...actionLog, actionRecord],
                    install: actionRecord.install,
                    installUpdatedAt: now.toISOString(),
                };

                await ctx.profiles.updateMeta(input.profileName, nextMeta);

                if (openAt) {
                    await ctx.profiles.updateStatus(profile.profileName, profile.status, {
                        preopenAt: preopenAt ? preopenAt.toISOString() : openAt.toISOString(),
                        openAt: openAt.toISOString(),
                        scheduledStartAt: scheduledAt,
                    });
                } else {
                    await ctx.profiles.updateStatus(profile.profileName, profile.status, {
                        preopenAt: null,
                        openAt: null,
                        scheduledStartAt: null,
                    });
                }

                return { ok: true, action: actionRecord };
            }),
        installNow: profileAdminProcedure
            .input(
                z.object({
                    profileName: z.string().min(1),
                    install: zInstallOptions,
                    reason: z.string().max(200).optional(),
                })
            )
            .mutation(async ({ ctx, input }) => {
                const adminAuth = requireAdminAuth(ctx);
                const profile = await ctx.profiles.getProfile(input.profileName);
                if (!profile) {
                    throw new TRPCError({
                        code: 'NOT_FOUND',
                        message: 'Profile not found.',
                    });
                }

                const scenarioValue = String(input.install.scenarioId);
                let updatedProfile = profile;
                if (profile.scenario !== scenarioValue) {
                    const updated = await ctx.profiles.updateScenario(profile.profileName, scenarioValue);
                    if (updated) {
                        updatedProfile = updated;
                    }
                }

                const databaseUrl = resolvePostgresConfigFromEnv({
                    env: process.env,
                    schema: updatedProfile.profile,
                }).url;
                const resourcesRoot = path.resolve(process.cwd(), 'resources');

                await seedProfileDatabase({
                    databaseUrl,
                    scenarioId: input.install.scenarioId,
                    tickSeconds: input.install.turnTermMinutes * 60,
                    now: new Date(),
                    installOptions: {
                        turnTermMinutes: input.install.turnTermMinutes,
                        sync: input.install.sync,
                        fiction: input.install.fiction,
                        extend: input.install.extend,
                        blockGeneralCreate: input.install.blockGeneralCreate,
                        npcMode: input.install.npcMode,
                        showImgLevel: input.install.showImgLevel,
                        tournamentTrig: input.install.tournamentTrig,
                        joinMode: input.install.joinMode,
                        autorunUser: input.install.autorunUser
                            ? {
                                  limitMinutes: input.install.autorunUser.limitMinutes,
                                  options: Object.fromEntries(
                                      input.install.autorunUser.options.map((option) => [option, true])
                                  ),
                              }
                            : null,
                    },
                    scenarioOptions: { scenarioRoot: path.join(resourcesRoot, 'scenario') },
                    mapOptions: { mapRoot: path.join(resourcesRoot, 'map') },
                    unitSetOptions: { unitSetRoot: path.join(resourcesRoot, 'unitset') },
                    adminUser: {
                        id: adminAuth.user.id,
                        username: adminAuth.user.username,
                        displayName: adminAuth.user.displayName,
                    },
                });

                await ctx.profiles.updateStatus(updatedProfile.profileName, 'RUNNING', {
                    preopenAt: null,
                    openAt: null,
                    scheduledStartAt: null,
                });

                return { ok: true };
            }),
        requestAction: adminProcedure
            .input(
                z.object({
                    profileName: z.string().min(1),
                    action: zServerAction,
                    durationMinutes: z.number().int().min(1).max(1440).optional(),
                    scheduledAt: z.string().datetime().optional(),
                    reason: z.string().max(200).optional(),
                })
            )
            .mutation(async ({ ctx, input }) => {
                const adminAuth = requireAdminAuth(ctx);
                if ((input.action === 'ACCELERATE' || input.action === 'DELAY') && !input.durationMinutes) {
                    throw new TRPCError({
                        code: 'BAD_REQUEST',
                        message: 'durationMinutes is required for acceleration or delay.',
                    });
                }
                if (input.action === 'RESET_SCHEDULED' && !input.scheduledAt) {
                    throw new TRPCError({
                        code: 'BAD_REQUEST',
                        message: 'scheduledAt is required for scheduled reset.',
                    });
                }
                const profile = await ctx.profiles.getProfile(input.profileName);
                if (!profile) {
                    throw new TRPCError({
                        code: 'NOT_FOUND',
                        message: 'Profile not found.',
                    });
                }

                const canManageProfiles = hasScopedPermission(adminAuth, ROLE_ADMIN_PROFILES, profile.profileName);
                const canResume =
                    canManageProfiles || hasScopedPermission(adminAuth, ROLE_RESUME_WHEN_STOPPED, profile.profileName);
                const canResetSchedule =
                    canManageProfiles || hasScopedPermission(adminAuth, ROLE_RESET_SCHEDULE, profile.profileName);
                const canOpenSurvey =
                    canManageProfiles || hasScopedPermission(adminAuth, ROLE_SURVEY_OPEN, profile.profileName);

                if (input.action === 'RESUME') {
                    if (profile.status !== 'STOPPED') {
                        throw new TRPCError({
                            code: 'BAD_REQUEST',
                            message: 'Resume is allowed only for STOPPED profiles.',
                        });
                    }
                    if (!canResume) {
                        throw new TRPCError({
                            code: 'FORBIDDEN',
                            message: 'Resume permission is required.',
                        });
                    }
                } else if (input.action === 'RESET_SCHEDULED') {
                    if (profile.status !== 'COMPLETED') {
                        throw new TRPCError({
                            code: 'BAD_REQUEST',
                            message: 'Reset scheduling is allowed only for COMPLETED profiles.',
                        });
                    }
                    if (!canResetSchedule) {
                        throw new TRPCError({
                            code: 'FORBIDDEN',
                            message: 'Reset scheduling permission is required.',
                        });
                    }
                } else if (input.action === 'OPEN_SURVEY') {
                    if (!canOpenSurvey) {
                        throw new TRPCError({
                            code: 'FORBIDDEN',
                            message: 'Survey permission is required.',
                        });
                    }
                } else if (!canManageProfiles) {
                    throw new TRPCError({
                        code: 'FORBIDDEN',
                        message: 'Profile management permission is required.',
                    });
                }

                const statusMap = {
                    RESUME: 'RUNNING',
                    PAUSE: 'PAUSED',
                    STOP: 'STOPPED',
                    SHUTDOWN: 'DISABLED',
                } as const;
                const mappedStatus = statusMap[input.action as keyof typeof statusMap];
                if (mappedStatus) {
                    await ctx.profiles.updateStatus(input.profileName, mappedStatus);
                    await ctx.orchestrator.reconcileNow();
                }

                const meta = readMetaObject(profile.meta);
                const actionLog = Array.isArray(meta.adminActions)
                    ? meta.adminActions.filter((entry) => entry && typeof entry === 'object')
                    : [];
                const actionRecord = {
                    action: input.action,
                    requestedAt: new Date().toISOString(),
                    durationMinutes: input.durationMinutes ?? null,
                    scheduledAt: input.scheduledAt ?? null,
                    reason: input.reason ?? null,
                    status: 'REQUESTED',
                };
                const nextMeta = {
                    ...meta,
                    adminActions: [...actionLog, actionRecord],
                };
                await ctx.profiles.updateMeta(input.profileName, nextMeta);
                return { ok: true, action: actionRecord };
            }),
        requestBuild: profileAdminProcedure
            .input(
                z.object({
                    profileName: z.string().min(1),
                    commitSha: z.string().min(7).max(64),
                })
            )
            .mutation(async ({ ctx, input }) => {
                const requestedAt = new Date().toISOString();
                const result = await ctx.profiles.updateBuildStatus(input.profileName, 'QUEUED', {
                    requestedAt,
                    error: null,
                    commitSha: input.commitSha,
                });
                return result;
            }),
        setBuildStatus: profileAdminProcedure
            .input(
                z.object({
                    profileName: z.string().min(1),
                    status: zBuildStatus,
                })
            )
            .mutation(async ({ ctx, input }) => ctx.profiles.updateBuildStatus(input.profileName, input.status)),
        reconcileNow: profileAdminProcedure.mutation(async ({ ctx }) => {
            await ctx.orchestrator.reconcileNow();
            return { ok: true };
        }),
        cleanupWorkspaces: profileAdminProcedure.mutation(async ({ ctx }) => {
            const result = await ctx.orchestrator.cleanupStaleWorkspaces();
            return {
                removed: result.removed,
                skipped: result.skipped,
            };
        }),
    }),
});
