<script setup lang="ts">
import {
    formatServerDateTime,
    serverDateTimeInputToIso,
    toServerDateTimeInputValue,
} from '@sammo-ts/common/time/ServerDateTime';
import { gatewayProfileCapabilities, type GatewayProfileStatus } from '@sammo-ts/common/gateway/profileStatus';
import { computed, onMounted, ref, watch } from 'vue';
import ServerProfileTabs from '../components/ServerProfileTabs.vue';
import AdminConsoleLayout from '../layouts/AdminConsoleLayout.vue';
import { useToast } from '../composables/useToast';
import {
    normalizeProfileResetDefaults,
    PROFILE_TURN_TERM_MINUTES,
    RESET_AUTORUN_LABELS,
    RESET_OPTION_COPY,
    type ProfileResetDefaults,
    type ResetAutorunOption,
} from '../utils/resetDefaults';
import { trpc } from '../utils/trpc';

type AdminSection = 'users' | 'servers' | 'system' | 'audit';

const props = defineProps<{
    section: AdminSection;
    profileName?: string;
}>();

const pageMeta: Record<AdminSection, { title: string; description: string; eyebrow: string }> = {
    users: {
        title: '사용자 관리',
        description: '계정 조회와 생성, 인증 유예, 권한, 제재, 복구 및 탈퇴 예약을 한 사용자 단위로 관리합니다.',
        eyebrow: 'Accounts and access',
    },
    servers: {
        title: '서버 관리',
        description: '프로필별 공개 정보와 계정 정책, 게임 실행 상태 및 운영 동작을 관리합니다.',
        eyebrow: 'Profile operations',
    },
    system: {
        title: '공지 · 접속',
        description: 'Gateway 공통 공지와 관리자 세션 연결 상태를 관리합니다.',
        eyebrow: 'Gateway settings',
    },
    audit: {
        title: '감사 로그',
        description: '관리자 조치의 실행 결과, 대상과 사유를 시간순으로 확인합니다.',
        eyebrow: 'Audit trail',
    },
};

const currentPage = computed(() => pageMeta[props.section]);

type AdminUserSanctions = {
    bannedUntil?: string;
    mutedUntil?: string;
    suspendedUntil?: string;
    warningCount?: number;
    flags?: string[];
    notes?: string;
    serverRestrictions?: Record<
        string,
        {
            blockedFeatures?: string[];
            until?: string;
            reason?: string;
            notes?: string;
        }
    >;
};

type AdminSanctionsPatch = {
    bannedUntil?: string | null;
    mutedUntil?: string | null;
    suspendedUntil?: string | null;
    warningCount?: number | null;
    flags?: string[] | null;
    notes?: string | null;
    serverRestrictions?: Record<
        string,
        {
            blockedFeatures?: string[];
            until?: string | null;
            reason?: string | null;
            notes?: string | null;
        } | null
    > | null;
};

type AdminUser = {
    id: string;
    username: string;
    displayName: string;
    roles: string[];
    sanctions: AdminUserSanctions;
    oauthType: string;
    oauthId?: string;
    email?: string;
    kakaoVerifiedAt?: string;
    kakaoGraceStartedAt: string;
    kakaoGraceUntil?: string;
    identityRevision?: string;
    kakaoReplacementApprovedUntil?: string;
    profileIconResetAt?: string;
    deleteAfter?: string;
    createdAt: string;
};

type AdminUserListItem = {
    id: string;
    username: string;
    displayName: string;
    email?: string;
    oauthType: 'NONE' | 'KAKAO';
    roles: string[];
    hasActiveSanction: boolean;
    deleteAfter?: string;
    createdAt: string;
};

type UserWorkspaceSection = 'account' | 'access' | 'restrictions' | 'lifecycle';

type AdminCapability = {
    permission: string;
    label: string;
    description: string;
    risk: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
    scope: 'GLOBAL' | 'PROFILE';
    scopes?: string[];
};

type AdminAuditEvent = {
    id: string;
    correlationId: string;
    actorUsername: string;
    targetType?: string;
    targetId?: string;
    profileName?: string;
    profileDisplayName?: string;
    action: string;
    outcome: 'STARTED' | 'SUCCEEDED' | 'FAILED';
    reason?: string;
    summary: Record<string, unknown>;
    errorMessage?: string;
    createdAt: string;
};

type KakaoGracePolicy = {
    profileName: string;
    profile: string;
    instanceKey: string;
    displayName: string;
    requiresKakaoVerification: boolean;
    kakaoVerified: boolean;
    accessAllowed: boolean;
    canCreateGeneral: boolean;
    graceEndsAt: string | null;
    generalCreationGraceDays: number;
    accessGraceDays: number;
    specialAccess: {
        kind: 'OPERATOR' | SpecialAccountAccessGrant['kind'];
        grantId: string | null;
        expiresAt: string | null;
        allowsGeneralCreation: boolean;
    } | null;
};

type SpecialAccountAccessGrant = {
    id: string;
    userId: string;
    kind: 'TESTER' | 'RECOVERY' | 'OTHER';
    profiles: string[];
    allowsGeneralCreation: boolean;
    expiresAt?: string;
    reason: string;
    grantedByUserId: string;
    revokedAt?: string;
    revokedReason?: string;
    createdAt: string;
};

type AdminPublicUser = {
    id: string;
    username: string;
    displayName: string;
    roles: string[];
    createdAt: string;
};

type AdminProfile = {
    profileName: string;
    profile: string;
    instanceKey: string;
    displayName?: string;
    currentScenario: string | null;
    /** @deprecated Rollback-compatible mirror of currentScenario. */
    scenario: string;
    status: GatewayProfileStatus;
    apiPort: number;
    runtime: {
        apiRunning: boolean;
        daemonRunning: boolean;
        auctionRunning: boolean;
        battleSimRunning: boolean;
        tournamentRunning: boolean;
    };
    buildCommitSha?: string;
    activeOperation?: {
        id: string;
        status: 'QUEUED' | 'RUNNING';
    } | null;
    meta: Record<string, unknown>;
    runtimeSettings: ProfileRuntimeSettings | null;
    runtimeActions: Array<{
        id: string;
        action: string;
        durationMinutes: number | null;
        status: 'REQUESTED' | 'PARTIAL' | 'APPLIED' | 'FAILED' | 'IGNORED';
        detail: string | null;
        handler: string | null;
        handledAt: string | null;
        createdAt: string;
        payload?: Record<string, unknown>;
    }>;
};

type ProfileRuntimeSettings = Pick<ProfileResetDefaults, 'turnTermMinutes' | 'blockGeneralCreate' | 'autorunUser'> & {
    isUnited: number;
};
type ProfileRuntimeSettingsUpdate = Omit<ProfileRuntimeSettings, 'isUnited'>;

type AdminAction =
    | 'RESUME'
    | 'PAUSE'
    | 'STOP'
    | 'CLOSE_COMPLETED'
    | 'ACCELERATE'
    | 'DELAY'
    | 'UPDATE_RUNTIME_SETTINGS'
    | 'RESET_NOW'
    | 'RESET_SCHEDULED'
    | 'SHUTDOWN';

type AdminClient = {
    capabilities: {
        list: { query: () => Promise<AdminCapability[]> };
    };
    audit: {
        list: { query: (input?: { limit?: number }) => Promise<AdminAuditEvent[]> };
    };
    system: {
        getNotice: {
            query: () => Promise<{ notice: string }>;
        };
        setNotice: {
            mutate: (input: { notice: string }) => Promise<{ notice: string }>;
        };
    };
    users: {
        list: {
            query: (input?: { query?: string; limit?: number; cursor?: string }) => Promise<{
                users: AdminUserListItem[];
                total: number;
                nextCursor?: string;
            }>;
        };
        getLocalAccountStatus: {
            query: () => Promise<{ enabled: boolean }>;
        };
        createLocal: {
            mutate: (input: {
                username: string;
                password: string;
                displayName?: string;
            }) => Promise<{ user: AdminPublicUser }>;
        };
        lookup: {
            query: (input: { id?: string; username?: string; email?: string }) => Promise<AdminUser | null>;
        };
        getKakaoGracePolicies: {
            query: (input: { userId: string }) => Promise<{
                kakaoVerified: boolean;
                kakaoGraceStartedAt: string;
                kakaoGraceUntil: string | null;
                specialAccessGrants: SpecialAccountAccessGrant[];
                profiles: KakaoGracePolicy[];
            }>;
        };
        updateKakaoGrace: {
            mutate: (input: { userId: string; until: string | null; reason: string }) => Promise<{
                kakaoGraceUntil: string | null;
            }>;
        };
        setKakaoReplacementApproval: {
            mutate: (input: { userId: string; until: string | null; reason: string }) => Promise<{
                kakaoReplacementApprovedUntil: string | null;
            }>;
        };
        updateIdentity: {
            mutate: (input: {
                userId: string;
                username: string;
                displayName: string;
                reason: string;
            }) => Promise<{ username: string; displayName: string; identityRevision?: string }>;
        };
        grantSpecialAccess: {
            mutate: (input: {
                userId: string;
                kind: SpecialAccountAccessGrant['kind'];
                profiles: string[];
                allowsGeneralCreation: boolean;
                expiresAt: string | null;
                reason: string;
            }) => Promise<SpecialAccountAccessGrant>;
        };
        revokeSpecialAccess: {
            mutate: (input: { userId: string; grantId: string; reason: string }) => Promise<SpecialAccountAccessGrant>;
        };
        listHistory: {
            query: (input: { userId: string; limit?: number }) => Promise<AdminAuditEvent[]>;
        };
        resetPassword: {
            mutate: (input: { userId: string; newPassword?: string; reason: string }) => Promise<{ password: string }>;
        };
        updateRoles: {
            mutate: (input: {
                userId: string;
                roles: string[];
                mode?: 'set' | 'grant' | 'revoke';
                reason: string;
            }) => Promise<{ roles: string[] }>;
        };
        updateSanctions: {
            mutate: (input: {
                userId: string;
                patch: AdminSanctionsPatch;
                reason: string;
            }) => Promise<{ sanctions: AdminUserSanctions }>;
        };
        setServerRestriction: {
            mutate: (input: {
                userId: string;
                profile: string;
                restriction: {
                    blockedFeatures?: string[];
                    until?: string | null;
                    reason?: string | null;
                    notes?: string | null;
                } | null;
                reason: string;
            }) => Promise<{ sanctions: AdminUserSanctions }>;
        };
        resetProfileIcon: {
            mutate: (input: { userId: string; reason: string }) => Promise<{
                profileIconResetAt: string;
                flushPublished: boolean;
            }>;
        };
        scheduleDeletion: {
            mutate: (input: { userId: string; retentionDays: number; reason: string }) => Promise<{
                ok: boolean;
                deleteAfter: string;
            }>;
        };
    };
    profiles: {
        list: {
            query: () => Promise<AdminProfile[]>;
        };
        updateMeta: {
            mutate: (input: {
                profileName: string;
                patch: {
                    korName?: string | null;
                    color?: string | null;
                    inGameNotice?: string | null;
                    profileImageUrl?: string | null;
                    nextSeasonIdx?: number | null;
                    firstGameIdx?: number | null;
                    localAccountAccessGraceDays?: number | null;
                    localAccountGeneralCreationGraceDays?: number | null;
                    resetDefaults?: ProfileResetDefaults | null;
                };
                reason: string;
            }) => Promise<AdminProfile | null>;
        };
        requestAction: {
            mutate: (input: {
                profileName: string;
                action: AdminAction;
                durationMinutes?: number;
                runtimeSettings?: ProfileRuntimeSettingsUpdate;
                scheduledAt?: string;
                reason?: string;
            }) => Promise<{ ok: boolean; action?: AdminProfile['runtimeActions'][number] }>;
        };
    };
};

const adminClient = trpc.admin as unknown as AdminClient;

const sessionToken = ref('');
const sessionTokenStatus = ref('');

if (typeof window !== 'undefined') {
    sessionToken.value = window.localStorage.getItem('sammo-session-token') ?? '';
}

const saveSessionToken = () => {
    const value = sessionToken.value.trim();
    if (typeof window !== 'undefined') {
        if (value) {
            window.localStorage.setItem('sammo-session-token', value);
        } else {
            window.localStorage.removeItem('sammo-session-token');
        }
    }
    sessionTokenStatus.value = value ? '저장됨' : '삭제됨';
};

const noticeDraft = ref('');
const noticeStatus = ref('');
const noticeLoading = ref(false);

const profiles = ref<AdminProfile[]>([]);
const profilesLoading = ref(false);
const profileEdits = ref<
    Record<
        string,
        {
            korName: string;
            color: string;
            inGameNotice: string;
            profileImageUrl: string;
            nextSeasonIdx: string | number;
            firstGameIdx: string | number;
            localAccountAccessGraceDays: string;
            localAccountGeneralCreationGraceDays: string;
            resetDefaults: ProfileResetDefaults;
            resetAutorunEnabled: boolean;
            reason: string;
        }
    >
>({});
const profileActions = ref<
    Record<
        string,
        {
            durationMinutes: string;
            scheduledAt: string;
            reason: string;
            turnTermMinutes: number;
            blockGeneralCreate: 0 | 1 | 2;
            autorunEnabled: boolean;
            autorunUserMinutes: string;
            autorunOptions: ResetAutorunOption[];
        }
    >
>({});
const profileActionStatus = ref<Record<string, string>>({});
const profileActionSubmitting = ref<Record<string, boolean>>({});
const visibleProfiles = computed(() =>
    props.profileName ? profiles.value.filter((profile) => profile.profileName === props.profileName) : profiles.value
);

const adminProfileDisplayName = (profile: AdminProfile): string => {
    if (profile.displayName?.trim()) return profile.displayName.trim();
    const configuredName = profile.meta.korName;
    const baseName =
        typeof configuredName === 'string' && configuredName.trim() ? configuredName.trim() : profile.profile;
    return profile.instanceKey === 'default' ? baseName : `${baseName} [${profile.instanceKey}]`;
};

const runtimeActionPending = (profile: AdminProfile): boolean => {
    return profile.runtimeActions.some((action) => action.status === 'REQUESTED' || action.status === 'PARTIAL');
};

const profileLifecycleText = (profile: AdminProfile): string => {
    if (profile.currentScenario === null) return 'DB 초기화 전 · 게임 접근 불가';
    if (profile.status === 'STOPPED') return '서버 프로세스 중지 · 게임 접근 불가 · 운영자 서버 재개 가능';
    if (profile.status === 'CANCELLED') return '취소 게임 · 접근 및 재개 불가 · 새 시나리오 초기화 필요';
    if (profile.status === 'DISABLED') return '비활성 · 게임 접근 불가';
    if (Number(profile.runtimeSettings?.isUnited ?? 0) !== 0)
        return '천하통일 완료 · 종료 기수 조회 가능 · 외부 목록 정리 대기';
    if (profile.status === 'PAUSED') return '턴 일시정지 · 게임 조회와 예약턴 입력 가능 · 운영자 재개 가능';
    if (profile.status === 'RUNNING') return '서버 운영 및 턴 진행 중';
    if (profile.status === 'PREOPEN') return '서버 접근 가능 · 개장 전 턴 정지';
    if (profile.status === 'COMPLETED') return '종료 기수 조회 가능 · 턴 정지';
    return '준비 중 · 게임 접근 불가';
};

const canResumeProfile = (profile: AdminProfile): boolean =>
    profile.currentScenario !== null && gatewayProfileCapabilities(profile.status).operatorResumable;
const canPauseProfile = (profile: AdminProfile): boolean => profile.status === 'RUNNING';
const canStopProfile = (profile: AdminProfile): boolean => gatewayProfileCapabilities(profile.status).runtimeExpected;
const canCloseCompletedProfile = (profile: AdminProfile): boolean =>
    gatewayProfileCapabilities(profile.status).runtimeExpected &&
    (profile.status === 'COMPLETED' || Number(profile.runtimeSettings?.isUnited ?? 0) !== 0);

const validDuration = (profileName: string): boolean => {
    const value = Number(profileActions.value[profileName]?.durationMinutes);
    return Number.isInteger(value) && value >= 1 && value <= 1440;
};

const validRuntimeSettings = (profileName: string): boolean => {
    const value = profileActions.value[profileName];
    if (!value || !PROFILE_TURN_TERM_MINUTES.some((minutes) => minutes === value.turnTermMinutes)) return false;
    if (!value.autorunEnabled) return true;
    const limitMinutes = Number(value.autorunUserMinutes);
    return (
        Number.isInteger(limitMinutes) && limitMinutes >= 1 && limitMinutes <= 43200 && value.autorunOptions.length > 0
    );
};

const runtimeActionStatusClass = (status: AdminProfile['runtimeActions'][number]['status']): string => {
    if (status === 'APPLIED') return 'text-emerald-400';
    if (status === 'FAILED') return 'text-red-400';
    if (status === 'IGNORED') return 'text-orange-400';
    if (status === 'PARTIAL') return 'text-amber-400';
    return 'text-zinc-400';
};

const isRuntimeActionTerminal = (status: AdminProfile['runtimeActions'][number]['status']): boolean =>
    status === 'APPLIED' || status === 'FAILED' || status === 'IGNORED';

const formatRuntimeActionTime = (value: string | null): string => formatServerDateTime(value);

const userLookupMode = ref<'username' | 'id' | 'email'>('username');
const userLookupValue = ref('');
const userLoading = ref(false);
const userError = ref('');
const userResult = ref<AdminUser | null>(null);
const userDirectoryQuery = ref('');
const userDirectory = ref<AdminUserListItem[]>([]);
const userDirectoryTotal = ref(0);
const userDirectoryNextCursor = ref<string>();
const userDirectoryLoading = ref(false);
const userDirectoryError = ref('');
const userWorkspaceSection = ref<UserWorkspaceSection>('account');

const userWorkspaceSections: Array<{ id: UserWorkspaceSection; label: string; description: string }> = [
    { id: 'account', label: '계정', description: '기본 정보와 로컬 계정 생성' },
    { id: 'access', label: '접근 · 권한', description: '비밀번호, 운영 권한, Kakao 접근' },
    { id: 'restrictions', label: '보안 · 제재', description: '차단, 서버 제한, 아이콘 초기화' },
    { id: 'lifecycle', label: '탈퇴 · 이력', description: '탈퇴 예약과 관리자 조치 이력' },
];

const localAccountEnabled = ref(false);
const localAccountStatus = ref('');
const localAccountResult = ref('');
const localAccountLoading = ref(false);
const localAccountForm = ref({
    username: '',
    password: '',
    displayName: '',
});

const passwordInput = ref('');
const passwordResult = ref('');
const passwordStatus = ref('');
const identityUsername = ref('');
const identityDisplayName = ref('');
const identityStatus = ref('');
const kakaoReplacementUntil = ref('');
const kakaoReplacementStatus = ref('');

const rolesInput = ref('');
const rolesMode = ref<'set' | 'grant' | 'revoke'>('grant');
const rolesStatus = ref('');
const capabilities = ref<AdminCapability[]>([]);
const hasCapability = (permission: string, profileName?: string): boolean =>
    capabilities.value.some((entry) => {
        if (entry.permission !== permission) return false;
        if (!profileName || entry.scope === 'GLOBAL') return true;
        return !entry.scopes?.length || entry.scopes.includes('*') || entry.scopes.includes(profileName);
    });
const selectedCapability = ref('');
const capabilityProfile = ref('');
const userActionReason = ref('');

const banUntil = ref('');
const banReason = ref('');
const banStatus = ref('');

const profileIconStatus = ref('');

const restrictionProfile = ref('');
const restrictionFeatures = ref('');
const restrictionUntil = ref('');
const restrictionReason = ref('');
const restrictionNotes = ref('');
const restrictionStatus = ref('');

const forceDeleteStatus = ref('');
const deletionRetentionDays = ref(30);
const kakaoGraceUntil = ref('');
const kakaoGraceStatus = ref('');
const kakaoPolicies = ref<KakaoGracePolicy[]>([]);
const specialAccessGrants = ref<SpecialAccountAccessGrant[]>([]);
const specialAccessKind = ref<SpecialAccountAccessGrant['kind']>('RECOVERY');
const specialAccessProfiles = ref<string[]>([]);
const specialAccessAllowsGeneralCreation = ref(true);
const specialAccessExpiresAt = ref('');
const specialAccessStatus = ref('');
const userHistory = ref<AdminAuditEvent[]>([]);
const globalAuditHistory = ref<AdminAuditEvent[]>([]);
const globalAuditStatus = ref('');

const profileScopeLabel = (scope: string): string => {
    const exact = kakaoPolicies.value.find((policy) => policy.profileName === scope);
    if (exact) return exact.displayName;
    const base = kakaoPolicies.value.filter((policy) => policy.profile === scope);
    if (!base.length) return '삭제되었거나 접근할 수 없는 서버';
    const baseName = base[0]!.displayName.replace(/ \[[^\]]+\]$/, '');
    return base.length > 1 ? `${baseName} 전체 인스턴스` : baseName;
};

const specialAccessScopeOptions = computed(() => {
    const grouped = new Map<string, KakaoGracePolicy[]>();
    for (const policy of kakaoPolicies.value) {
        const bucket = grouped.get(policy.profile) ?? [];
        bucket.push(policy);
        grouped.set(policy.profile, bucket);
    }
    return [...grouped.entries()].flatMap(([profile, entries]) => {
        if (entries.length === 1) return [{ value: profile, label: entries[0]!.displayName }];
        const baseName = entries[0]!.displayName.replace(/ \[[^\]]+\]$/, '');
        return [
            { value: profile, label: `${baseName} 전체 인스턴스` },
            ...entries.map((entry) => ({ value: entry.profileName, label: entry.displayName })),
        ];
    });
});

const auditTargetLabel = (event: AdminAuditEvent): string => {
    if (event.profileDisplayName) return event.profileDisplayName;
    if (event.targetType === 'PROFILE' || event.profileName) return '삭제되었거나 접근할 수 없는 서버';
    return event.targetId ?? '';
};

const { feedback: showFeedbackToast } = useToast();
const actionFeedback = [
    noticeStatus,
    userError,
    kakaoGraceStatus,
    identityStatus,
    kakaoReplacementStatus,
    specialAccessStatus,
    passwordStatus,
    rolesStatus,
    banStatus,
    profileIconStatus,
    restrictionStatus,
    forceDeleteStatus,
];

watch(
    actionFeedback,
    (current, previous) => {
        current.forEach((message, index) => {
            if (message && message !== previous[index]) showFeedbackToast(message);
        });
    },
    { flush: 'sync' }
);

const setLocalAccountFeedback = (message: string): void => {
    localAccountStatus.value = message;
    showFeedbackToast(message);
};

watch(
    profileActionStatus,
    (current, previous) => {
        Object.entries(current).forEach(([profileName, message]) => {
            if (message && message !== previous[profileName]) showFeedbackToast(message);
        });
    },
    { flush: 'sync' }
);

const hasUser = computed(() => Boolean(userResult.value));

const loadLocalAccountStatus = async () => {
    localAccountStatus.value = '';
    try {
        const result = await adminClient.users.getLocalAccountStatus.query();
        localAccountEnabled.value = result.enabled;
        localAccountStatus.value = result.enabled ? '' : 'ENV 설정이 비활성화 상태입니다.';
    } catch (error) {
        localAccountEnabled.value = false;
        localAccountStatus.value = '로컬 계정 생성 설정 확인 실패';
    }
};

const loadNotice = async () => {
    noticeLoading.value = true;
    try {
        const result = await adminClient.system.getNotice.query();
        noticeDraft.value = result.notice;
    } catch (error) {
        noticeStatus.value = '공지 불러오기 실패';
    } finally {
        noticeLoading.value = false;
    }
};

const saveNotice = async () => {
    noticeLoading.value = true;
    noticeStatus.value = '';
    try {
        const result = await adminClient.system.setNotice.mutate({
            notice: noticeDraft.value,
        });
        noticeDraft.value = result.notice;
        noticeStatus.value = '저장 완료';
    } catch (error) {
        noticeStatus.value = '저장 실패';
    } finally {
        noticeLoading.value = false;
    }
};

const ensureProfileBuffers = (profile: AdminProfile) => {
    if (!profileEdits.value[profile.profileName]) {
        const meta = (profile.meta ?? {}) as Record<string, unknown>;
        const resetDefaults = normalizeProfileResetDefaults(meta.resetDefaults);
        profileEdits.value[profile.profileName] = {
            korName: String(meta.korName ?? profile.profile),
            color: String(meta.color ?? '#ffffff'),
            inGameNotice: String(meta.inGameNotice ?? ''),
            profileImageUrl: String(meta.profileImageUrl ?? ''),
            nextSeasonIdx:
                typeof meta.nextSeasonIdx === 'number' && Number.isFinite(meta.nextSeasonIdx)
                    ? String(Math.floor(meta.nextSeasonIdx))
                    : '',
            firstGameIdx:
                typeof meta.firstGameIdx === 'number' && Number.isFinite(meta.firstGameIdx)
                    ? String(Math.floor(meta.firstGameIdx))
                    : '1',
            localAccountAccessGraceDays:
                typeof meta.localAccountAccessGraceDays === 'number'
                    ? String(Math.floor(meta.localAccountAccessGraceDays))
                    : '',
            localAccountGeneralCreationGraceDays:
                typeof meta.localAccountGeneralCreationGraceDays === 'number'
                    ? String(Math.floor(meta.localAccountGeneralCreationGraceDays))
                    : '',
            resetDefaults,
            resetAutorunEnabled: resetDefaults.autorunUser !== null,
            reason: '',
        };
    }
    if (!profileActions.value[profile.profileName]) {
        const settings = profile.runtimeSettings ?? normalizeProfileResetDefaults(profile.meta?.resetDefaults);
        profileActions.value[profile.profileName] = {
            durationMinutes: '',
            scheduledAt: '',
            reason: '',
            turnTermMinutes: settings.turnTermMinutes,
            blockGeneralCreate: settings.blockGeneralCreate,
            autorunEnabled: settings.autorunUser !== null,
            autorunUserMinutes: String(settings.autorunUser?.limitMinutes ?? 1440),
            autorunOptions: settings.autorunUser?.options.slice() ?? RESET_AUTORUN_LABELS.map(({ value }) => value),
        };
    }
};

const syncRuntimeSettingsBuffer = (profile: AdminProfile): void => {
    const settings = profile.runtimeSettings;
    const buffer = profileActions.value[profile.profileName];
    if (!settings || !buffer) return;
    buffer.turnTermMinutes = settings.turnTermMinutes;
    buffer.blockGeneralCreate = settings.blockGeneralCreate;
    buffer.autorunEnabled = settings.autorunUser !== null;
    buffer.autorunUserMinutes = String(settings.autorunUser?.limitMinutes ?? 1440);
    buffer.autorunOptions = settings.autorunUser?.options.slice() ?? RESET_AUTORUN_LABELS.map(({ value }) => value);
};

const toLocalInputValue = (value: string): string => toServerDateTimeInputValue(value);

const loadProfiles = async () => {
    profilesLoading.value = true;
    try {
        const result = await adminClient.profiles.list.query();
        result.forEach((profile) => {
            ensureProfileBuffers(profile);
            const latest = profile.runtimeActions[0];
            if (latest && isRuntimeActionTerminal(latest.status)) {
                profileActionStatus.value = {
                    ...profileActionStatus.value,
                    [profile.profileName]: '',
                };
            }
        });
        profiles.value = result;
    } catch (error) {
        profileActionStatus.value = {
            ...profileActionStatus.value,
            global: '프로필 목록을 불러오지 못했습니다.',
        };
    } finally {
        profilesLoading.value = false;
    }
};

const refreshRuntimeActionUntilTerminal = async (profileName: string, actionId: string): Promise<void> => {
    for (let attempt = 0; attempt < 20; attempt += 1) {
        const current = profiles.value
            .find((profile) => profile.profileName === profileName)
            ?.runtimeActions.find((action) => action.id === actionId);
        if (current && isRuntimeActionTerminal(current.status)) {
            const profile = profiles.value.find((item) => item.profileName === profileName);
            if (profile && current.status === 'APPLIED') syncRuntimeSettingsBuffer(profile);
            profileActionStatus.value = {
                ...profileActionStatus.value,
                [profileName]: '',
            };
            return;
        }
        await new Promise((resolve) => setTimeout(resolve, 1_000));
        try {
            const result = await adminClient.profiles.list.query();
            result.forEach((profile) => {
                ensureProfileBuffers(profile);
            });
            profiles.value = result;
            const refreshedAction = result
                .find((profile) => profile.profileName === profileName)
                ?.runtimeActions.find((action) => action.id === actionId);
            if (refreshedAction?.status === 'APPLIED') {
                const profile = result.find((item) => item.profileName === profileName);
                if (profile) syncRuntimeSettingsBuffer(profile);
            }
        } catch {
            // 일시적인 조회 실패는 다음 bounded poll에서 다시 확인합니다.
        }
    }
};

const updateProfileMeta = async (profileName: string) => {
    const edit = profileEdits.value[profileName];
    if (!edit) {
        return;
    }
    // Vue casts non-empty values from type="number" inputs to numbers even when
    // the buffer was initialized with a string. Normalize both runtime shapes so
    // event season 0 reaches the metadata mutation instead of throwing on trim().
    const nextSeasonRaw = String(edit.nextSeasonIdx).trim();
    const nextSeasonIdx = nextSeasonRaw === '' ? null : Number(nextSeasonRaw);
    if (nextSeasonIdx !== null && (!Number.isFinite(nextSeasonIdx) || nextSeasonIdx < 0)) {
        profileActionStatus.value = {
            ...profileActionStatus.value,
            [profileName]: '다음 시즌 번호는 0 이상 숫자여야 합니다.',
        };
        return;
    }
    const firstGameIdxRaw = String(edit.firstGameIdx).trim();
    const firstGameIdx = firstGameIdxRaw === '' ? null : Number(firstGameIdxRaw);
    if (firstGameIdx !== null && (!Number.isInteger(firstGameIdx) || firstGameIdx < 0)) {
        profileActionStatus.value = {
            ...profileActionStatus.value,
            [profileName]: '첫 기수 번호는 0 이상 정수여야 합니다.',
        };
        return;
    }
    const readGraceDays = (value: string): number | null => {
        if (!value.trim()) return null;
        const parsed = Number(value);
        return Number.isInteger(parsed) && parsed >= 0 && parsed <= 365 ? parsed : Number.NaN;
    };
    const accessGraceDays = readGraceDays(edit.localAccountAccessGraceDays);
    const creationGraceDays = readGraceDays(edit.localAccountGeneralCreationGraceDays);
    if (Number.isNaN(accessGraceDays) || Number.isNaN(creationGraceDays)) {
        profileActionStatus.value = {
            ...profileActionStatus.value,
            [profileName]: 'Kakao 유예일은 0~365 사이 정수여야 합니다.',
        };
        return;
    }
    if (edit.reason.trim().length < 3) {
        profileActionStatus.value = { ...profileActionStatus.value, [profileName]: '변경 사유를 입력하세요.' };
        return;
    }
    if (
        edit.resetAutorunEnabled &&
        (!edit.resetDefaults.autorunUser ||
            edit.resetDefaults.autorunUser.limitMinutes <= 0 ||
            edit.resetDefaults.autorunUser.limitMinutes > 43200 ||
            edit.resetDefaults.autorunUser.options.length === 0)
    ) {
        profileActionStatus.value = {
            ...profileActionStatus.value,
            [profileName]: '자율행동은 유효 시간과 한 개 이상의 행동을 선택해야 합니다.',
        };
        return;
    }
    const patch = {
        korName: edit.korName.trim() || null,
        color: edit.color.trim() || null,
        inGameNotice: edit.inGameNotice.trim() || null,
        profileImageUrl: edit.profileImageUrl.trim() || null,
        nextSeasonIdx: nextSeasonIdx === null ? null : Math.floor(nextSeasonIdx),
        firstGameIdx,
        localAccountAccessGraceDays: accessGraceDays,
        localAccountGeneralCreationGraceDays: creationGraceDays,
        resetDefaults: {
            ...edit.resetDefaults,
            autorunUser: edit.resetAutorunEnabled ? edit.resetDefaults.autorunUser : null,
        },
    };
    try {
        const updated = await adminClient.profiles.updateMeta.mutate({
            profileName,
            patch,
            reason: edit.reason.trim(),
        });
        profileActionStatus.value = {
            ...profileActionStatus.value,
            [profileName]: updated ? '메타 저장 완료' : '메타 저장 실패',
        };
        if (updated) {
            profiles.value = profiles.value.map((item) =>
                item.profileName === profileName ? { ...item, ...updated } : item
            );
        }
    } catch (error) {
        const detail = error instanceof Error ? error.message : '';
        profileActionStatus.value = {
            ...profileActionStatus.value,
            [profileName]: detail ? `메타 저장 실패: ${detail}` : '메타 저장 실패',
        };
    }
};

const ensureResetAutorun = (profileName: string) => {
    const edit = profileEdits.value[profileName];
    if (!edit?.resetAutorunEnabled || edit.resetDefaults.autorunUser) return;
    edit.resetDefaults.autorunUser = {
        limitMinutes: 1440,
        options: RESET_AUTORUN_LABELS.map(({ value }) => value),
    };
};

const requestProfileAction = async (profileName: string, action: AdminAction) => {
    if (profileActionSubmitting.value[profileName]) {
        return;
    }
    profileActionSubmitting.value = {
        ...profileActionSubmitting.value,
        [profileName]: true,
    };
    const actionState = profileActions.value[profileName];
    const timeShiftAction = action === 'ACCELERATE' || action === 'DELAY';
    const durationMinutes =
        timeShiftAction && actionState?.durationMinutes ? Number(actionState.durationMinutes) : undefined;
    const durationValue = durationMinutes && validDuration(profileName) ? durationMinutes : undefined;
    const scheduledAt =
        action === 'RESET_SCHEDULED' && actionState?.scheduledAt
            ? serverDateTimeInputToIso(actionState.scheduledAt)
            : undefined;
    const reason = actionState?.reason.trim() || undefined;
    const runtimeSettings: ProfileRuntimeSettingsUpdate | undefined =
        action === 'UPDATE_RUNTIME_SETTINGS' && actionState
            ? {
                  turnTermMinutes: actionState.turnTermMinutes,
                  blockGeneralCreate: actionState.blockGeneralCreate,
                  autorunUser: actionState.autorunEnabled
                      ? {
                            limitMinutes: Number(actionState.autorunUserMinutes),
                            options: actionState.autorunOptions,
                        }
                      : null,
              }
            : undefined;
    if (action === 'UPDATE_RUNTIME_SETTINGS' && (!validRuntimeSettings(profileName) || !reason || reason.length < 3)) {
        profileActionStatus.value = {
            ...profileActionStatus.value,
            [profileName]:
                !reason || reason.length < 3
                    ? '현재 기수 설정 변경 사유를 입력하세요.'
                    : '턴 시간과 자율행동 값을 확인하세요.',
        };
        profileActionSubmitting.value = { ...profileActionSubmitting.value, [profileName]: false };
        return;
    }
    let runtimeActionId: string | undefined;
    try {
        const result = await adminClient.profiles.requestAction.mutate({
            profileName,
            action,
            durationMinutes: durationValue,
            runtimeSettings,
            scheduledAt,
            reason,
        });
        profileActionStatus.value = {
            ...profileActionStatus.value,
            [profileName]:
                result.action && (action === 'ACCELERATE' || action === 'DELAY')
                    ? `접수됨 · ${result.action.status} · ${action} ${result.action.durationMinutes ?? ''}분`
                    : result.action && action === 'UPDATE_RUNTIME_SETTINGS'
                      ? `접수됨 · ${result.action.status} · 현재 기수 설정 변경`
                      : `요청 접수: ${action}`,
        };
        if (result.action) {
            runtimeActionId = result.action.id;
            await loadProfiles();
        }
    } catch (error) {
        profileActionStatus.value = {
            ...profileActionStatus.value,
            [profileName]: `요청 실패: ${action}`,
        };
    } finally {
        profileActionSubmitting.value = {
            ...profileActionSubmitting.value,
            [profileName]: false,
        };
    }
    if (runtimeActionId) {
        void refreshRuntimeActionUntilTerminal(profileName, runtimeActionId);
    }
};

const lookupUser = async () => {
    userLoading.value = true;
    userError.value = '';
    passwordStatus.value = '';
    rolesStatus.value = '';
    banStatus.value = '';
    restrictionStatus.value = '';
    profileIconStatus.value = '';
    forceDeleteStatus.value = '';
    try {
        const payload =
            userLookupMode.value === 'id'
                ? { id: userLookupValue.value.trim() }
                : userLookupMode.value === 'email'
                  ? { email: userLookupValue.value.trim() }
                  : { username: userLookupValue.value.trim() };
        const result = await adminClient.users.lookup.query(payload);
        if (!result) {
            userResult.value = null;
            userError.value = '사용자를 찾을 수 없습니다.';
            return;
        }
        userResult.value = result;
        identityUsername.value = result.username;
        identityDisplayName.value = result.displayName;
        kakaoReplacementUntil.value = result.kakaoReplacementApprovedUntil
            ? toLocalInputValue(result.kakaoReplacementApprovedUntil)
            : toLocalInputValue(new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString());
        const [grace, history] = await Promise.all([
            adminClient.users.getKakaoGracePolicies.query({ userId: result.id }),
            adminClient.users.listHistory.query({ userId: result.id, limit: 50 }),
        ]);
        kakaoPolicies.value = grace.profiles;
        specialAccessGrants.value = grace.specialAccessGrants;
        kakaoGraceUntil.value = grace.kakaoGraceUntil ? toLocalInputValue(grace.kakaoGraceUntil) : '';
        userHistory.value = history;
    } catch (error) {
        userError.value = '조회 실패';
    } finally {
        userLoading.value = false;
    }
};

const loadUserDirectory = async (append = false) => {
    userDirectoryLoading.value = true;
    userDirectoryError.value = '';
    try {
        const result = await adminClient.users.list.query({
            query: userDirectoryQuery.value.trim() || undefined,
            limit: 30,
            cursor: append ? userDirectoryNextCursor.value : undefined,
        });
        userDirectory.value = append ? [...userDirectory.value, ...result.users] : result.users;
        userDirectoryTotal.value = result.total;
        userDirectoryNextCursor.value = result.nextCursor;
    } catch {
        userDirectoryError.value = '계정 목록을 불러오지 못했습니다.';
    } finally {
        userDirectoryLoading.value = false;
    }
};

const selectDirectoryUser = async (user: AdminUserListItem) => {
    userLookupMode.value = 'id';
    userLookupValue.value = user.id;
    await lookupUser();
};

const requireUserActionReason = (): string | null => {
    const reason = userActionReason.value.trim();
    if (reason.length < 3) {
        userError.value = '민감한 관리자 조치에는 3자 이상의 사유가 필요합니다.';
        return null;
    }
    return reason;
};

const loadGlobalAudit = async () => {
    if (!capabilities.value.some((entry) => entry.permission === 'admin.audit.read')) return;
    try {
        globalAuditHistory.value = await adminClient.audit.list.query({ limit: 100 });
        globalAuditStatus.value = '';
    } catch {
        globalAuditStatus.value = '감사 원장을 불러오지 못했습니다.';
    }
};

const refreshUserHistory = async () => {
    if (!userResult.value) return;
    userHistory.value = await adminClient.users.listHistory.query({ userId: userResult.value.id, limit: 50 });
    await loadGlobalAudit();
};

const loadCapabilities = async () => {
    try {
        capabilities.value = await adminClient.capabilities.list.query();
        selectedCapability.value = capabilities.value[0]?.permission ?? '';
        if (props.section === 'users' || props.section === 'audit') await loadGlobalAudit();
    } catch {
        capabilities.value = [];
    }
};

const applyCapabilitySelection = () => {
    const capability = capabilities.value.find((entry) => entry.permission === selectedCapability.value);
    if (!capability) return;
    if (capability.scope === 'PROFILE' && !capabilityProfile.value.trim()) {
        rolesStatus.value = '대상 서버를 선택하세요.';
        return;
    }
    rolesInput.value =
        capability.scope === 'PROFILE'
            ? `${capability.permission}:${capabilityProfile.value.trim()}`
            : capability.permission;
};

const updateKakaoGrace = async (clear = false) => {
    if (!userResult.value) return;
    const reason = requireUserActionReason();
    if (!reason) return;
    try {
        const result = await adminClient.users.updateKakaoGrace.mutate({
            userId: userResult.value.id,
            until: clear || !kakaoGraceUntil.value ? null : (serverDateTimeInputToIso(kakaoGraceUntil.value) ?? null),
            reason,
        });
        userResult.value = {
            ...userResult.value,
            kakaoGraceUntil: result.kakaoGraceUntil ?? undefined,
        };
        kakaoGraceStatus.value = result.kakaoGraceUntil ? 'OAuth 유예 연장 완료' : '개별 유예 해제 완료';
        const grace = await adminClient.users.getKakaoGracePolicies.query({ userId: userResult.value.id });
        kakaoPolicies.value = grace.profiles;
        specialAccessGrants.value = grace.specialAccessGrants;
        await Promise.all([refreshUserHistory(), loadUserDirectory()]);
    } catch {
        kakaoGraceStatus.value = 'OAuth 유예 변경 실패';
    }
};

const updateUserIdentity = async () => {
    if (!userResult.value) return;
    const reason = requireUserActionReason();
    if (!reason) return;
    identityStatus.value = '';
    try {
        const result = await adminClient.users.updateIdentity.mutate({
            userId: userResult.value.id,
            username: identityUsername.value,
            displayName: identityDisplayName.value,
            reason,
        });
        userResult.value = { ...userResult.value, ...result };
        identityUsername.value = result.username;
        identityDisplayName.value = result.displayName;
        identityStatus.value = 'ID와 닉네임을 변경했습니다.';
        await Promise.all([refreshUserHistory(), loadUserDirectory()]);
    } catch {
        identityStatus.value = 'ID 또는 닉네임 변경에 실패했습니다.';
    }
};

const setKakaoReplacementApproval = async (clear = false) => {
    if (!userResult.value) return;
    const reason = requireUserActionReason();
    if (!reason) return;
    const until = clear ? null : (serverDateTimeInputToIso(kakaoReplacementUntil.value) ?? null);
    if (!clear && !until) {
        kakaoReplacementStatus.value = '올바른 교체 승인 만료 시각을 입력하세요.';
        return;
    }
    try {
        const result = await adminClient.users.setKakaoReplacementApproval.mutate({
            userId: userResult.value.id,
            until,
            reason,
        });
        userResult.value = {
            ...userResult.value,
            kakaoReplacementApprovedUntil: result.kakaoReplacementApprovedUntil ?? undefined,
        };
        kakaoReplacementStatus.value = result.kakaoReplacementApprovedUntil
            ? '새 카카오 계정 교체를 승인했습니다.'
            : '카카오 계정 교체 승인을 해제했습니다.';
        await refreshUserHistory();
    } catch {
        kakaoReplacementStatus.value = '카카오 계정 교체 승인 변경에 실패했습니다.';
    }
};

const grantSpecialAccess = async () => {
    if (!userResult.value) return;
    const reason = requireUserActionReason();
    if (!reason) return;
    specialAccessStatus.value = '';
    try {
        await adminClient.users.grantSpecialAccess.mutate({
            userId: userResult.value.id,
            kind: specialAccessKind.value,
            profiles: specialAccessProfiles.value,
            allowsGeneralCreation: specialAccessAllowsGeneralCreation.value,
            expiresAt: specialAccessExpiresAt.value
                ? (serverDateTimeInputToIso(specialAccessExpiresAt.value) ?? null)
                : null,
            reason,
        });
        const policy = await adminClient.users.getKakaoGracePolicies.query({ userId: userResult.value.id });
        kakaoPolicies.value = policy.profiles;
        specialAccessGrants.value = policy.specialAccessGrants;
        specialAccessStatus.value = '특수 접근 자격을 부여했습니다.';
        await Promise.all([refreshUserHistory(), loadUserDirectory()]);
    } catch {
        specialAccessStatus.value = '특수 접근 자격 부여에 실패했습니다.';
    }
};

const revokeSpecialAccess = async (grantId: string) => {
    if (!userResult.value) return;
    const reason = requireUserActionReason();
    if (!reason) return;
    try {
        await adminClient.users.revokeSpecialAccess.mutate({ userId: userResult.value.id, grantId, reason });
        const policy = await adminClient.users.getKakaoGracePolicies.query({ userId: userResult.value.id });
        kakaoPolicies.value = policy.profiles;
        specialAccessGrants.value = policy.specialAccessGrants;
        specialAccessStatus.value = '특수 접근 자격을 해제했습니다.';
        await Promise.all([refreshUserHistory(), loadUserDirectory()]);
    } catch {
        specialAccessStatus.value = '특수 접근 자격 해제에 실패했습니다.';
    }
};

const resetUserPassword = async () => {
    if (!userResult.value) {
        return;
    }
    const reason = requireUserActionReason();
    if (!reason) return;
    passwordStatus.value = '';
    passwordResult.value = '';
    try {
        const result = await adminClient.users.resetPassword.mutate({
            userId: userResult.value.id,
            newPassword: passwordInput.value.trim() || undefined,
            reason,
        });
        passwordResult.value = result.password;
        passwordStatus.value = '초기화 완료';
        passwordInput.value = '';
        await Promise.all([refreshUserHistory(), loadUserDirectory()]);
    } catch (error) {
        passwordStatus.value = '초기화 실패';
    }
};

const updateUserRoles = async () => {
    if (!userResult.value) {
        return;
    }
    const reason = requireUserActionReason();
    if (!reason) return;
    const roles = rolesInput.value
        .split(',')
        .map((role) => role.trim())
        .filter(Boolean);
    if (!roles.length) {
        rolesStatus.value = '역할을 입력하세요.';
        return;
    }
    rolesStatus.value = '';
    try {
        const result = await adminClient.users.updateRoles.mutate({
            userId: userResult.value.id,
            roles,
            mode: rolesMode.value,
            reason,
        });
        userResult.value = { ...userResult.value, roles: result.roles };
        rolesStatus.value = '권한 업데이트 완료';
        await Promise.all([refreshUserHistory(), loadUserDirectory()]);
    } catch (error) {
        rolesStatus.value = '권한 업데이트 실패';
    }
};

const applyBan = async () => {
    if (!userResult.value) {
        return;
    }
    const reason = requireUserActionReason();
    if (!reason) return;
    const until = banUntil.value ? (serverDateTimeInputToIso(banUntil.value) ?? null) : null;
    const patch = {
        bannedUntil: until,
        notes: banReason.value.trim() || undefined,
    };
    try {
        const result = await adminClient.users.updateSanctions.mutate({
            userId: userResult.value.id,
            patch,
            reason,
        });
        userResult.value = { ...userResult.value, sanctions: result.sanctions };
        banStatus.value = '차단 설정 완료';
        await Promise.all([refreshUserHistory(), loadUserDirectory()]);
    } catch (error) {
        banStatus.value = '차단 설정 실패';
    }
};

const clearBan = async () => {
    if (!userResult.value) {
        return;
    }
    const reason = requireUserActionReason();
    if (!reason) return;
    try {
        const result = await adminClient.users.updateSanctions.mutate({
            userId: userResult.value.id,
            patch: { bannedUntil: null },
            reason,
        });
        userResult.value = { ...userResult.value, sanctions: result.sanctions };
        banStatus.value = '차단 해제 완료';
        await Promise.all([refreshUserHistory(), loadUserDirectory()]);
    } catch (error) {
        banStatus.value = '차단 해제 실패';
    }
};

const resetProfileIcon = async () => {
    if (!userResult.value) {
        return;
    }
    const reason = requireUserActionReason();
    if (!reason) return;
    try {
        const result = await adminClient.users.resetProfileIcon.mutate({
            userId: userResult.value.id,
            reason,
        });
        userResult.value = {
            ...userResult.value,
            profileIconResetAt: result.profileIconResetAt,
        };
        profileIconStatus.value = result.flushPublished
            ? '아이콘 초기화 요청 완료'
            : '아이콘은 초기화됐지만 실행 중 서버 알림에 실패했습니다. 다시 요청해 주세요.';
        await Promise.all([refreshUserHistory(), loadUserDirectory()]);
    } catch (error) {
        profileIconStatus.value = '아이콘 초기화 실패';
    }
};

const applyRestriction = async () => {
    if (!userResult.value) {
        return;
    }
    const reason = requireUserActionReason();
    if (!reason) return;
    if (!restrictionProfile.value.trim()) {
        restrictionStatus.value = '대상 서버를 선택하세요.';
        return;
    }
    const features = restrictionFeatures.value
        .split(',')
        .map((feature) => feature.trim())
        .filter(Boolean);
    const restriction = {
        blockedFeatures: features.length ? features : undefined,
        until: restrictionUntil.value ? serverDateTimeInputToIso(restrictionUntil.value) : undefined,
        reason: restrictionReason.value.trim() || undefined,
        notes: restrictionNotes.value.trim() || undefined,
    };
    try {
        const result = await adminClient.users.setServerRestriction.mutate({
            userId: userResult.value.id,
            profile: restrictionProfile.value.trim(),
            restriction,
            reason,
        });
        userResult.value = { ...userResult.value, sanctions: result.sanctions };
        restrictionStatus.value = '서버 제재 적용 완료';
        await Promise.all([refreshUserHistory(), loadUserDirectory()]);
    } catch (error) {
        restrictionStatus.value = '서버 제재 적용 실패';
    }
};

const clearRestriction = async () => {
    if (!userResult.value) {
        return;
    }
    const reason = requireUserActionReason();
    if (!reason) return;
    if (!restrictionProfile.value.trim()) {
        restrictionStatus.value = '대상 서버를 선택하세요.';
        return;
    }
    try {
        const result = await adminClient.users.setServerRestriction.mutate({
            userId: userResult.value.id,
            profile: restrictionProfile.value.trim(),
            restriction: null,
            reason,
        });
        userResult.value = { ...userResult.value, sanctions: result.sanctions };
        restrictionStatus.value = '서버 제재 해제 완료';
        await Promise.all([refreshUserHistory(), loadUserDirectory()]);
    } catch (error) {
        restrictionStatus.value = '서버 제재 해제 실패';
    }
};

const scheduleDeleteUser = async () => {
    if (!userResult.value) {
        return;
    }
    const reason = requireUserActionReason();
    if (!reason) return;
    if (typeof window !== 'undefined') {
        const confirmed = window.confirm(`${deletionRetentionDays.value}일 보존 후 탈퇴하도록 예약하시겠습니까?`);
        if (!confirmed) {
            return;
        }
    }
    try {
        const result = await adminClient.users.scheduleDeletion.mutate({
            userId: userResult.value.id,
            retentionDays: deletionRetentionDays.value,
            reason,
        });
        userResult.value = { ...userResult.value, deleteAfter: result.deleteAfter };
        forceDeleteStatus.value = `탈퇴 예약 완료: ${formatServerDateTime(result.deleteAfter)}`;
        await Promise.all([refreshUserHistory(), loadUserDirectory()]);
    } catch (error) {
        forceDeleteStatus.value = '탈퇴 예약 실패';
    }
};

const createLocalAccount = async () => {
    localAccountStatus.value = '';
    localAccountResult.value = '';
    if (!localAccountEnabled.value) {
        setLocalAccountFeedback('ENV 설정이 비활성화 상태입니다.');
        return;
    }
    const username = localAccountForm.value.username.trim();
    const password = localAccountForm.value.password.trim();
    const displayName = localAccountForm.value.displayName.trim();
    if (!username || !password) {
        setLocalAccountFeedback('아이디와 비밀번호를 입력하세요.');
        return;
    }
    localAccountLoading.value = true;
    try {
        const result = await adminClient.users.createLocal.mutate({
            username,
            password,
            displayName: displayName || undefined,
        });
        localAccountResult.value = `생성됨: ${result.user.username} (${result.user.id})`;
        setLocalAccountFeedback('로컬 계정 생성 완료');
        localAccountForm.value = {
            username: result.user.username,
            password: '',
            displayName: '',
        };
        userLookupMode.value = 'username';
        userLookupValue.value = result.user.username;
        await Promise.all([lookupUser(), loadUserDirectory()]);
    } catch (error) {
        setLocalAccountFeedback('로컬 계정 생성 실패');
    } finally {
        localAccountLoading.value = false;
    }
};

onMounted(() => {
    if (props.section === 'users' || props.section === 'audit' || props.section === 'servers') {
        void loadCapabilities();
    }
    if (props.section === 'users') {
        void loadUserDirectory();
        void loadLocalAccountStatus();
    }
    if (props.section === 'system') {
        void loadNotice();
    }
    if (props.section === 'servers') {
        void loadProfiles();
    }
});
</script>

<template>
    <AdminConsoleLayout
        :title="currentPage.title"
        :description="currentPage.description"
        :eyebrow="currentPage.eyebrow"
    >
        <div class="space-y-8">
            <section v-if="section === 'system'" class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-3">
                <div class="flex items-center justify-between">
                    <h3 class="text-lg font-semibold">관리자 세션 토큰</h3>
                    <span class="text-xs text-zinc-500">{{ sessionTokenStatus }}</span>
                </div>
                <div class="flex flex-col md:flex-row gap-3">
                    <input
                        v-model="sessionToken"
                        type="password"
                        class="flex-1 bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-yellow-500"
                        placeholder="세션 토큰 입력"
                    />
                    <button
                        class="bg-yellow-600 hover:bg-yellow-500 text-black font-semibold px-4 py-2 rounded"
                        @click="saveSessionToken"
                    >
                        저장
                    </button>
                </div>
            </section>

            <div class="space-y-8">
                <section v-if="section === 'users'" class="grid min-w-0 items-start gap-6 xl:grid-cols-2">
                    <div class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4 xl:col-span-2">
                        <div class="flex flex-wrap items-end justify-between gap-2">
                            <div>
                                <h3 class="text-lg font-semibold">계정 디렉터리</h3>
                                <p class="mt-1 text-xs text-zinc-500">
                                    최근 가입 계정을 먼저 보여줍니다. 계정명·표시명·이메일·UUID 일부로 검색할 수
                                    있습니다.
                                </p>
                            </div>
                            <span class="text-xs text-zinc-400">총 {{ userDirectoryTotal }}개</span>
                        </div>
                        <form class="flex flex-col gap-2 md:flex-row" @submit.prevent="loadUserDirectory(false)">
                            <input
                                v-model="userDirectoryQuery"
                                type="search"
                                class="flex-1 bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-yellow-500"
                                placeholder="계정명, 표시명, 이메일 또는 UUID 검색"
                            />
                            <button
                                class="bg-blue-700 hover:bg-blue-600 disabled:opacity-50 text-white font-semibold px-4 py-2 rounded"
                                :disabled="userDirectoryLoading"
                            >
                                목록 검색
                            </button>
                        </form>
                        <div v-if="userDirectoryError" class="text-xs text-red-400">{{ userDirectoryError }}</div>
                        <div
                            v-if="userDirectory.length"
                            class="max-h-[28rem] overflow-y-auto rounded border border-zinc-800 bg-zinc-950"
                            role="region"
                            aria-label="계정 목록"
                        >
                            <button
                                v-for="user in userDirectory"
                                :key="user.id"
                                type="button"
                                class="grid w-full gap-1 border-b border-zinc-800 px-4 py-3 text-left last:border-b-0 hover:bg-zinc-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-yellow-500 md:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_auto] md:items-center"
                                :class="
                                    userResult?.id === user.id ? 'bg-zinc-900 ring-1 ring-inset ring-yellow-700' : ''
                                "
                                @click="selectDirectoryUser(user)"
                            >
                                <span class="min-w-0">
                                    <span class="block truncate text-sm font-semibold text-white">{{
                                        user.username
                                    }}</span>
                                    <span class="block truncate text-xs text-zinc-400">{{ user.displayName }}</span>
                                </span>
                                <span class="min-w-0 text-xs text-zinc-500">
                                    <span class="block truncate">{{ user.email || '이메일 없음' }}</span>
                                    <span
                                        >{{ user.oauthType }} ·
                                        {{ formatServerDateTime(user.createdAt, { format: 'date' }) }}</span
                                    >
                                </span>
                                <span class="flex flex-wrap gap-1 md:justify-end">
                                    <span
                                        v-if="user.hasActiveSanction"
                                        class="rounded bg-red-950 px-2 py-1 text-[11px] text-red-300"
                                        >제재 중</span
                                    >
                                    <span
                                        v-if="user.deleteAfter"
                                        class="rounded bg-orange-950 px-2 py-1 text-[11px] text-orange-300"
                                        >탈퇴 예약</span
                                    >
                                    <span class="rounded bg-zinc-800 px-2 py-1 text-[11px] text-zinc-300">{{
                                        user.roles[0] || '역할 없음'
                                    }}</span>
                                </span>
                            </button>
                        </div>
                        <div
                            v-else-if="!userDirectoryLoading"
                            class="rounded border border-dashed border-zinc-700 p-6 text-center text-sm text-zinc-500"
                        >
                            조건에 맞는 계정이 없습니다.
                        </div>
                        <button
                            v-if="userDirectoryNextCursor"
                            type="button"
                            class="w-full rounded border border-zinc-700 px-4 py-2 text-sm text-zinc-300 hover:bg-zinc-800 disabled:opacity-50"
                            :disabled="userDirectoryLoading"
                            @click="loadUserDirectory(true)"
                        >
                            더 보기
                        </button>

                        <details class="rounded border border-zinc-800 bg-black/20 p-3">
                            <summary class="cursor-pointer text-xs font-semibold text-zinc-400">
                                정확한 값으로 직접 열기
                            </summary>
                            <form class="space-y-3" @submit.prevent="lookupUser">
                                <div class="flex flex-col md:flex-row gap-2">
                                    <select
                                        v-model="userLookupMode"
                                        class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                    >
                                        <option value="username">계정명</option>
                                        <option value="email">이메일</option>
                                        <option value="id">UUID</option>
                                    </select>
                                    <input
                                        v-model="userLookupValue"
                                        type="text"
                                        class="flex-1 bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-yellow-500"
                                        placeholder="검색 값 입력"
                                    />
                                    <button
                                        class="bg-blue-700 hover:bg-blue-600 text-white font-semibold px-4 py-2 rounded"
                                        :disabled="userLoading"
                                    >
                                        조회
                                    </button>
                                </div>
                                <div v-if="userError" class="text-xs text-red-400">{{ userError }}</div>
                            </form>
                        </details>

                        <div v-if="userResult" class="bg-zinc-950 border border-zinc-800 rounded p-4 space-y-2">
                            <div class="flex justify-between items-center">
                                <div class="text-sm font-semibold">{{ userResult.username }}</div>
                                <div class="text-xs text-zinc-500">{{ userResult.id }}</div>
                            </div>
                            <div class="text-xs text-zinc-400">표시명: {{ userResult.displayName }}</div>
                            <div class="text-xs text-zinc-400">권한: {{ userResult.roles.join(', ') || '-' }}</div>
                            <div class="text-xs text-zinc-500">
                                OAuth: {{ userResult.oauthType }} {{ userResult.email ?? '' }}
                            </div>
                            <div class="text-xs text-zinc-500">
                                Kakao 인증: {{ userResult.kakaoVerifiedAt ? '완료' : '미완료' }} · 유예 시작:
                                {{ formatServerDateTime(userResult.kakaoGraceStartedAt) }}
                            </div>
                            <div v-if="userResult.kakaoGraceUntil" class="text-xs text-amber-300">
                                관리자 유예: {{ formatServerDateTime(userResult.kakaoGraceUntil) }}까지
                            </div>
                            <div v-if="userResult.kakaoReplacementApprovedUntil" class="text-xs text-amber-300">
                                Kakao 교체 승인:
                                {{ formatServerDateTime(userResult.kakaoReplacementApprovedUntil) }}까지
                            </div>
                            <div v-if="userResult.deleteAfter" class="text-xs text-red-300">
                                탈퇴 예약: {{ formatServerDateTime(userResult.deleteAfter) }}
                            </div>
                            <div class="text-xs text-zinc-500">
                                가입일: {{ formatServerDateTime(userResult.createdAt) }}
                            </div>
                            <div class="text-xs text-zinc-400 mt-2">제재 상태</div>
                            <pre class="text-[11px] text-zinc-400 bg-black/50 p-2 rounded whitespace-pre-wrap"
                                >{{ JSON.stringify(userResult.sanctions, null, 2) }}
              </pre>
                        </div>
                    </div>

                    <div
                        v-if="hasUser"
                        class="bg-zinc-900 border border-amber-800/70 rounded-lg p-5 space-y-3 xl:col-span-2"
                    >
                        <h4 class="text-base font-semibold">민감 조치 공통 사유</h4>
                        <input
                            v-model="userActionReason"
                            type="text"
                            maxlength="200"
                            class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                            placeholder="권한·제재·복구·탈퇴 조치 사유 (필수)"
                        />
                        <div class="text-xs text-zinc-500">사유와 정화된 입력은 관리자 감사 원장에 기록됩니다.</div>
                    </div>

                    <nav
                        v-if="hasUser"
                        class="grid gap-2 rounded-lg border border-zinc-800 bg-zinc-900 p-2 sm:grid-cols-2 xl:col-span-2 xl:grid-cols-4"
                        aria-label="사용자 관리 기능"
                    >
                        <button
                            v-for="item in userWorkspaceSections"
                            :key="item.id"
                            type="button"
                            class="rounded px-4 py-3 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-yellow-500"
                            :class="
                                userWorkspaceSection === item.id
                                    ? 'bg-yellow-600 text-black'
                                    : 'bg-zinc-950 text-zinc-300 hover:bg-zinc-800'
                            "
                            :aria-current="userWorkspaceSection === item.id ? 'page' : undefined"
                            @click="userWorkspaceSection = item.id"
                        >
                            <span class="block text-sm font-semibold">{{ item.label }}</span>
                            <span class="mt-1 block text-[11px] opacity-75">{{ item.description }}</span>
                        </button>
                    </nav>

                    <div
                        v-if="userWorkspaceSection === 'account'"
                        class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4 xl:col-span-2"
                    >
                        <div class="flex items-center justify-between">
                            <h4 class="text-base font-semibold">로컬 계정 생성</h4>
                            <span class="text-xs text-zinc-500"> ENV {{ localAccountEnabled ? 'ON' : 'OFF' }} </span>
                        </div>
                        <div class="text-xs text-zinc-500">카카오 OAuth 없이 로그인 가능한 계정을 생성합니다.</div>
                        <div class="grid gap-2">
                            <input
                                v-model="localAccountForm.username"
                                type="text"
                                class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                placeholder="아이디"
                                :disabled="!localAccountEnabled || localAccountLoading"
                            />
                            <input
                                v-model="localAccountForm.password"
                                type="password"
                                class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                placeholder="비밀번호"
                                :disabled="!localAccountEnabled || localAccountLoading"
                            />
                            <input
                                v-model="localAccountForm.displayName"
                                type="text"
                                class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                placeholder="표시명 (선택)"
                                :disabled="!localAccountEnabled || localAccountLoading"
                            />
                            <button
                                class="bg-cyan-600 hover:bg-cyan-500 text-black font-semibold px-4 py-2 rounded"
                                :disabled="!localAccountEnabled || localAccountLoading"
                                @click="createLocalAccount"
                            >
                                계정 생성
                            </button>
                        </div>
                        <div class="text-xs text-zinc-500">{{ localAccountStatus }}</div>
                        <div v-if="localAccountResult" class="text-xs text-emerald-400">
                            {{ localAccountResult }}
                        </div>
                    </div>

                    <div
                        v-if="userWorkspaceSection === 'account' && hasUser"
                        class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4"
                    >
                        <h4 class="text-base font-semibold">ID · 닉네임 변경</h4>
                        <p class="text-xs text-zinc-400">
                            사용자는 직접 바꿀 수 없습니다. 닉네임은 현재 장수에 반영되며 과거 장수와 명예의 전당의 당시
                            기록은 유지됩니다.
                        </p>
                        <div class="grid gap-2">
                            <label class="text-xs text-zinc-400" for="admin-identity-username">로그인 ID</label>
                            <input
                                id="admin-identity-username"
                                v-model="identityUsername"
                                type="text"
                                class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                            />
                            <label class="text-xs text-zinc-400" for="admin-identity-display-name">닉네임</label>
                            <input
                                id="admin-identity-display-name"
                                v-model="identityDisplayName"
                                type="text"
                                class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                            />
                            <button
                                type="button"
                                class="bg-blue-700 hover:bg-blue-600 text-white font-semibold px-4 py-2 rounded"
                                @click="updateUserIdentity"
                            >
                                ID · 닉네임 변경
                            </button>
                        </div>
                        <div class="text-xs text-zinc-500">{{ identityStatus }}</div>
                    </div>

                    <div
                        v-if="userWorkspaceSection === 'account' && hasUser && userResult?.oauthType === 'KAKAO'"
                        class="bg-zinc-900 border border-red-900/70 rounded-lg p-5 space-y-4"
                    >
                        <h4 class="text-base font-semibold">Kakao 계정 영구 교체</h4>
                        <p class="text-xs text-zinc-400">
                            승인 뒤 사용자가 계정 관리에서 새 Kakao 계정의 소유권을 직접 증명합니다. 교체가 완료되면
                            기존 Kakao stable ID는 영구 폐기되어 로그인·재가입·재연결에 쓸 수 없습니다.
                        </p>
                        <input
                            v-model="kakaoReplacementUntil"
                            type="datetime-local"
                            class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                            aria-label="Kakao 계정 교체 승인 만료 시각"
                        />
                        <div class="flex flex-col gap-2 sm:flex-row">
                            <button
                                type="button"
                                class="flex-1 bg-amber-600 hover:bg-amber-500 text-black font-semibold px-4 py-2 rounded"
                                @click="setKakaoReplacementApproval(false)"
                            >
                                교체 승인
                            </button>
                            <button
                                type="button"
                                class="flex-1 bg-zinc-700 hover:bg-zinc-600 px-4 py-2 rounded"
                                @click="setKakaoReplacementApproval(true)"
                            >
                                승인 해제
                            </button>
                        </div>
                        <div class="text-xs text-zinc-500">{{ kakaoReplacementStatus }}</div>
                    </div>

                    <div
                        v-if="userWorkspaceSection === 'access'"
                        class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4"
                    >
                        <h4 class="text-base font-semibold">비밀번호 리셋</h4>
                        <div class="flex flex-col md:flex-row gap-2">
                            <input
                                v-model="passwordInput"
                                type="text"
                                class="flex-1 bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                placeholder="직접 지정 시 입력"
                                :disabled="!hasUser"
                            />
                            <button
                                class="bg-emerald-600 hover:bg-emerald-500 text-black font-semibold px-4 py-2 rounded"
                                :disabled="!hasUser"
                                @click="resetUserPassword"
                            >
                                초기화
                            </button>
                        </div>
                        <div class="text-xs text-zinc-400">임시 비밀번호: {{ passwordResult || '-' }}</div>
                        <div class="text-xs text-zinc-500">{{ passwordStatus }}</div>
                    </div>

                    <div
                        v-if="userWorkspaceSection === 'access'"
                        class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4 xl:col-span-2"
                    >
                        <h4 class="text-base font-semibold">특수 권한 부여</h4>
                        <div class="grid gap-2 md:grid-cols-[1fr_1fr_auto]">
                            <select
                                v-model="selectedCapability"
                                class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                :disabled="!hasUser"
                            >
                                <option
                                    v-for="capability in capabilities"
                                    :key="capability.permission"
                                    :value="capability.permission"
                                >
                                    {{ capability.label }} · {{ capability.risk }}
                                </option>
                            </select>
                            <select
                                v-model="capabilityProfile"
                                class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                :disabled="!hasUser || !kakaoPolicies.length"
                                aria-label="권한 대상 서버"
                            >
                                <option value="">대상 서버 선택</option>
                                <option
                                    v-for="policy in kakaoPolicies"
                                    :key="policy.profileName"
                                    :value="policy.profileName"
                                >
                                    {{ policy.displayName }}
                                </option>
                            </select>
                            <button
                                class="bg-zinc-700 hover:bg-zinc-600 px-3 py-2 rounded text-sm"
                                :disabled="!hasUser"
                                @click="applyCapabilitySelection"
                            >
                                선택 반영
                            </button>
                        </div>
                        <div v-if="selectedCapability" class="text-xs text-zinc-500">
                            {{ capabilities.find((item) => item.permission === selectedCapability)?.description }}
                        </div>
                        <div class="flex flex-col md:flex-row gap-2">
                            <select
                                v-model="rolesMode"
                                class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                :disabled="!hasUser"
                            >
                                <option value="grant">추가</option>
                                <option value="revoke">제거</option>
                                <option value="set">덮어쓰기</option>
                            </select>
                            <input
                                v-model="rolesInput"
                                type="text"
                                class="flex-1 bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                placeholder="admin, moderator 등"
                                :disabled="!hasUser"
                            />
                            <button
                                class="bg-indigo-600 hover:bg-indigo-500 text-white font-semibold px-4 py-2 rounded"
                                :disabled="!hasUser"
                                @click="updateUserRoles"
                            >
                                적용
                            </button>
                        </div>
                        <div class="text-xs text-zinc-500">{{ rolesStatus }}</div>
                    </div>

                    <div
                        v-if="userWorkspaceSection === 'access'"
                        class="bg-zinc-900 border border-amber-800/60 rounded-lg p-5 space-y-4"
                    >
                        <h4 class="text-base font-semibold">Kakao 없는 특수 계정 접근</h4>
                        <div class="text-xs text-zinc-400">
                            운영자 role은 자동으로 모든 서버에 접근합니다. 테스트·복구·기타 계정은 아래에서 서버 범위와
                            만료를 명시해 부여합니다. 복구 자격은 만료가 필수이며 최대 90일입니다. 시각 입력은 서버 시간
                            UTC+9 기준입니다.
                        </div>
                        <div class="grid grid-cols-1 md:grid-cols-2 gap-2">
                            <select
                                v-model="specialAccessKind"
                                class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                :disabled="!hasUser"
                            >
                                <option value="RECOVERY">휴대폰 분실·계정 복구</option>
                                <option value="TESTER">특수 테스트</option>
                                <option value="OTHER">기타 예외</option>
                            </select>
                            <input
                                v-model="specialAccessExpiresAt"
                                type="datetime-local"
                                class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                :disabled="!hasUser"
                                aria-label="특수 접근 만료 시각"
                            />
                            <fieldset class="rounded border border-zinc-700 bg-zinc-950 px-3 py-2 text-sm text-white">
                                <legend class="px-1 text-xs text-zinc-400">허용 서버 (비우면 전체)</legend>
                                <label
                                    v-for="option in specialAccessScopeOptions"
                                    :key="option.value"
                                    class="mr-4 inline-flex items-center gap-2"
                                >
                                    <input
                                        v-model="specialAccessProfiles"
                                        type="checkbox"
                                        :value="option.value"
                                        :disabled="!hasUser"
                                    />
                                    {{ option.label }}
                                </label>
                            </fieldset>
                            <label class="flex items-center gap-2 text-sm text-zinc-300 px-2">
                                <input
                                    v-model="specialAccessAllowsGeneralCreation"
                                    type="checkbox"
                                    :disabled="!hasUser"
                                />
                                장수 생성 허용
                            </label>
                        </div>
                        <button
                            class="bg-amber-600 hover:bg-amber-500 text-black font-semibold px-4 py-2 rounded"
                            :disabled="!hasUser"
                            @click="grantSpecialAccess"
                        >
                            특수 접근 부여
                        </button>
                        <div class="text-xs text-zinc-500">{{ specialAccessStatus }}</div>
                        <div v-if="specialAccessGrants.length" class="space-y-2">
                            <div
                                v-for="grant in specialAccessGrants"
                                :key="grant.id"
                                class="bg-black/30 border border-zinc-800 rounded p-3 text-xs space-y-1"
                            >
                                <div class="flex flex-wrap items-center justify-between gap-2">
                                    <span class="font-semibold text-amber-200">
                                        {{ grant.kind }} ·
                                        {{
                                            grant.profiles.length
                                                ? grant.profiles.map(profileScopeLabel).join(', ')
                                                : '전체 서버'
                                        }}
                                    </span>
                                    <button
                                        v-if="!grant.revokedAt"
                                        class="bg-red-900 hover:bg-red-800 text-red-100 px-3 py-1 rounded"
                                        @click="revokeSpecialAccess(grant.id)"
                                    >
                                        해제
                                    </button>
                                </div>
                                <div>
                                    장수 생성 {{ grant.allowsGeneralCreation ? '허용' : '차단' }} · 만료
                                    {{ formatServerDateTime(grant.expiresAt, { fallback: '없음' }) }}
                                </div>
                                <div class="text-zinc-500">부여 사유: {{ grant.reason }}</div>
                                <div v-if="grant.revokedAt" class="text-red-300">
                                    해제됨: {{ formatServerDateTime(grant.revokedAt) }} ·
                                    {{ grant.revokedReason }}
                                </div>
                            </div>
                        </div>
                    </div>

                    <div
                        v-if="userWorkspaceSection === 'access'"
                        class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4"
                    >
                        <h4 class="text-base font-semibold">Kakao 인증 유예</h4>
                        <div class="text-xs text-zinc-500">
                            기본·서버별 유예가 끝난 사용자를 예외적으로 더 허용할 때 사용합니다. 시각 입력은 서버 시간
                            UTC+9 기준입니다.
                        </div>
                        <div class="flex flex-col md:flex-row gap-2">
                            <input
                                v-model="kakaoGraceUntil"
                                type="datetime-local"
                                class="flex-1 bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                :disabled="!hasUser"
                            />
                            <button
                                class="bg-yellow-600 hover:bg-yellow-500 text-black px-4 py-2 rounded"
                                :disabled="!hasUser"
                                @click="updateKakaoGrace(false)"
                            >
                                유예 연장
                            </button>
                            <button
                                class="bg-zinc-700 hover:bg-zinc-600 px-4 py-2 rounded"
                                :disabled="!hasUser"
                                @click="updateKakaoGrace(true)"
                            >
                                개별 유예 해제
                            </button>
                        </div>
                        <div class="text-xs text-zinc-500">{{ kakaoGraceStatus }}</div>
                        <div class="overflow-x-auto">
                            <table class="w-full min-w-[620px] text-xs">
                                <thead class="text-zinc-500">
                                    <tr>
                                        <th class="p-2 text-left">서버</th>
                                        <th>접근</th>
                                        <th>장수 생성</th>
                                        <th>기본 접근 유예</th>
                                        <th>특수 자격</th>
                                        <th>종료</th>
                                    </tr>
                                </thead>
                                <tbody>
                                    <tr
                                        v-for="policy in kakaoPolicies"
                                        :key="policy.profileName"
                                        class="border-t border-zinc-800"
                                    >
                                        <td class="p-2">{{ policy.displayName }}</td>
                                        <td class="text-center">{{ policy.accessAllowed ? '허용' : '차단' }}</td>
                                        <td class="text-center">{{ policy.canCreateGeneral ? '허용' : '차단' }}</td>
                                        <td class="text-center">{{ policy.accessGraceDays }}일</td>
                                        <td class="text-center">{{ policy.specialAccess?.kind ?? '-' }}</td>
                                        <td class="text-center">
                                            {{ policy.graceEndsAt ? formatServerDateTime(policy.graceEndsAt) : '-' }}
                                        </td>
                                    </tr>
                                </tbody>
                            </table>
                        </div>
                    </div>

                    <div
                        v-if="userWorkspaceSection === 'restrictions'"
                        class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4"
                    >
                        <h4 class="text-base font-semibold">유저 차단 (서버 시간 UTC+9)</h4>
                        <div class="flex flex-col gap-2">
                            <input
                                v-model="banUntil"
                                type="datetime-local"
                                class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                :disabled="!hasUser"
                            />
                            <input
                                v-model="banReason"
                                type="text"
                                class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                placeholder="사유"
                                :disabled="!hasUser"
                            />
                            <div class="flex gap-2">
                                <button
                                    class="bg-red-600 hover:bg-red-500 text-white font-semibold px-4 py-2 rounded"
                                    :disabled="!hasUser"
                                    @click="applyBan"
                                >
                                    차단 설정
                                </button>
                                <button
                                    class="bg-zinc-700 hover:bg-zinc-600 text-white font-semibold px-4 py-2 rounded"
                                    :disabled="!hasUser"
                                    @click="clearBan"
                                >
                                    차단 해제
                                </button>
                            </div>
                        </div>
                        <div class="text-xs text-zinc-500">{{ banStatus }}</div>
                    </div>

                    <div
                        v-if="userWorkspaceSection === 'restrictions'"
                        class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4"
                    >
                        <h4 class="text-base font-semibold">서버별 기능 제재 (서버 시간 UTC+9)</h4>
                        <div class="grid gap-2">
                            <select
                                v-model="restrictionProfile"
                                class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                :disabled="!hasUser || !kakaoPolicies.length"
                                aria-label="제재 대상 서버"
                            >
                                <option value="">대상 서버 선택</option>
                                <option
                                    v-for="policy in kakaoPolicies"
                                    :key="policy.profileName"
                                    :value="policy.profileName"
                                >
                                    {{ policy.displayName }}
                                </option>
                            </select>
                            <input
                                v-model="restrictionFeatures"
                                type="text"
                                class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                placeholder="login, message 등"
                                :disabled="!hasUser"
                            />
                            <input
                                v-model="restrictionUntil"
                                type="datetime-local"
                                class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                :disabled="!hasUser"
                            />
                            <input
                                v-model="restrictionReason"
                                type="text"
                                class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                placeholder="사유"
                                :disabled="!hasUser"
                            />
                            <input
                                v-model="restrictionNotes"
                                type="text"
                                class="bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                placeholder="메모"
                                :disabled="!hasUser"
                            />
                            <div class="flex gap-2">
                                <button
                                    class="bg-amber-600 hover:bg-amber-500 text-black font-semibold px-4 py-2 rounded"
                                    :disabled="!hasUser"
                                    @click="applyRestriction"
                                >
                                    제재 적용
                                </button>
                                <button
                                    class="bg-zinc-700 hover:bg-zinc-600 text-white font-semibold px-4 py-2 rounded"
                                    :disabled="!hasUser"
                                    @click="clearRestriction"
                                >
                                    제재 해제
                                </button>
                            </div>
                        </div>
                        <div class="text-xs text-zinc-500">{{ restrictionStatus }}</div>
                    </div>

                    <div
                        v-if="userWorkspaceSection === 'restrictions'"
                        class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4 xl:col-span-2"
                    >
                        <h4 class="text-base font-semibold">프로필 아이콘 초기화</h4>
                        <button
                            class="bg-purple-600 hover:bg-purple-500 text-white font-semibold px-4 py-2 rounded"
                            :disabled="!hasUser"
                            @click="resetProfileIcon"
                        >
                            아이콘 초기화 요청
                        </button>
                        <div class="text-xs text-zinc-500">{{ profileIconStatus }}</div>
                    </div>

                    <div
                        v-if="userWorkspaceSection === 'lifecycle'"
                        class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4"
                    >
                        <h4 class="text-base font-semibold text-red-400">관리자 탈퇴 예약</h4>
                        <input
                            v-model.number="deletionRetentionDays"
                            type="number"
                            min="1"
                            max="90"
                            class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                            aria-label="탈퇴 전 보존 일수"
                            :disabled="!hasUser"
                        />
                        <button
                            class="bg-red-700 hover:bg-red-600 text-white font-semibold px-4 py-2 rounded"
                            :disabled="!hasUser"
                            @click="scheduleDeleteUser"
                        >
                            보존 기간 후 탈퇴 예약
                        </button>
                        <div class="text-xs text-zinc-500">{{ forceDeleteStatus }}</div>
                    </div>

                    <div
                        v-if="userWorkspaceSection === 'lifecycle'"
                        class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4"
                    >
                        <h4 class="text-base font-semibold">사용자 관리자 조치 이력</h4>
                        <div v-if="!userHistory.length" class="text-xs text-zinc-500">기록이 없습니다.</div>
                        <div v-else class="max-h-80 overflow-auto space-y-2">
                            <div
                                v-for="event in userHistory"
                                :key="event.id"
                                class="border border-zinc-800 rounded p-3 text-xs"
                            >
                                <div class="flex justify-between gap-3">
                                    <span
                                        :class="
                                            event.outcome === 'FAILED'
                                                ? 'text-red-300'
                                                : event.outcome === 'SUCCEEDED'
                                                  ? 'text-emerald-300'
                                                  : 'text-amber-300'
                                        "
                                    >
                                        {{ event.outcome }} · {{ event.action }}
                                    </span>
                                    <span class="text-zinc-500">{{ formatServerDateTime(event.createdAt) }}</span>
                                </div>
                                <div class="text-zinc-400">
                                    {{ event.actorUsername }} · {{ event.reason ?? '사유 없음' }}
                                </div>
                                <div v-if="event.errorMessage" class="text-red-300">{{ event.errorMessage }}</div>
                                <pre class="mt-2 overflow-auto whitespace-pre-wrap text-[11px] text-zinc-500">{{
                                    JSON.stringify(event.summary, null, 2)
                                }}</pre>
                            </div>
                        </div>
                    </div>
                </section>

                <section v-if="section !== 'users'" class="min-w-0 space-y-6">
                    <div
                        v-if="
                            section === 'audit' && capabilities.some((entry) => entry.permission === 'admin.audit.read')
                        "
                        class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4"
                    >
                        <div class="flex items-center justify-between gap-3">
                            <h3 class="text-lg font-semibold">전체 관리자 감사 원장</h3>
                            <button
                                class="rounded bg-zinc-700 px-3 py-2 text-xs hover:bg-zinc-600"
                                @click="loadGlobalAudit"
                            >
                                새로고침
                            </button>
                        </div>
                        <div v-if="globalAuditStatus" class="text-xs text-red-400">{{ globalAuditStatus }}</div>
                        <div v-if="!globalAuditHistory.length" class="text-xs text-zinc-500">기록이 없습니다.</div>
                        <div v-else class="max-h-96 space-y-2 overflow-auto">
                            <div
                                v-for="event in globalAuditHistory"
                                :key="event.id"
                                class="rounded border border-zinc-800 p-3 text-xs"
                            >
                                <div class="flex justify-between gap-3">
                                    <span
                                        :class="
                                            event.outcome === 'FAILED'
                                                ? 'text-red-300'
                                                : event.outcome === 'SUCCEEDED'
                                                  ? 'text-emerald-300'
                                                  : 'text-amber-300'
                                        "
                                    >
                                        {{ event.outcome }} · {{ event.action }}
                                    </span>
                                    <span class="text-zinc-500">{{ formatServerDateTime(event.createdAt) }}</span>
                                </div>
                                <div class="text-zinc-400">
                                    {{ event.actorUsername }} · {{ event.targetType ?? '-' }}
                                    {{ auditTargetLabel(event) }} · {{ event.reason ?? '사유 없음' }}
                                </div>
                            </div>
                        </div>
                    </div>

                    <div
                        v-if="
                            section === 'audit' &&
                            !capabilities.some((entry) => entry.permission === 'admin.audit.read')
                        "
                        class="rounded-lg border border-zinc-800 bg-zinc-900 p-5 text-sm text-zinc-400"
                    >
                        감사 로그를 조회할 권한이 없습니다.
                    </div>

                    <div
                        v-if="section === 'system'"
                        class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4"
                    >
                        <div class="flex justify-between items-center">
                            <h3 class="text-lg font-semibold">서버 공지</h3>
                            <span class="text-xs text-zinc-500">{{ noticeStatus }}</span>
                        </div>
                        <textarea
                            v-model="noticeDraft"
                            class="w-full bg-zinc-950 border border-zinc-700 rounded p-3 text-sm text-white min-h-[140px]"
                            placeholder="로비 공지 입력"
                        />
                        <div class="flex gap-2">
                            <button
                                class="bg-blue-700 hover:bg-blue-600 text-white font-semibold px-4 py-2 rounded"
                                :disabled="noticeLoading"
                                @click="saveNotice"
                            >
                                공지 저장
                            </button>
                            <button
                                class="bg-zinc-700 hover:bg-zinc-600 text-white font-semibold px-4 py-2 rounded"
                                :disabled="noticeLoading"
                                @click="loadNotice"
                            >
                                다시 불러오기
                            </button>
                        </div>
                    </div>

                    <div v-if="section === 'servers'" class="space-y-4">
                        <div class="flex flex-wrap items-center justify-between gap-2">
                            <h3 class="text-lg font-semibold">서버별 관리</h3>
                            <div class="flex items-center gap-2">
                                <RouterLink
                                    v-if="hasCapability('admin.profiles.deploy')"
                                    to="/admin/releases/batch"
                                    class="rounded bg-sky-700 px-3 py-1.5 text-sm font-semibold text-white hover:bg-sky-600"
                                >
                                    일괄 업데이트
                                </RouterLink>
                                <button
                                    class="bg-zinc-700 hover:bg-zinc-600 text-white text-sm px-3 py-1.5 rounded"
                                    :disabled="profilesLoading"
                                    @click="loadProfiles"
                                >
                                    새로고침
                                </button>
                            </div>
                        </div>
                        <div
                            v-for="profile in visibleProfiles"
                            :key="profile.profileName"
                            class="bg-zinc-900 border border-zinc-800 rounded-lg p-5 space-y-4"
                        >
                            <ServerProfileTabs
                                :profile-name="profile.profileName"
                                :profile-label="adminProfileDisplayName(profile)"
                                active-tab="status"
                                :can-deploy="hasCapability('admin.profiles.deploy', profile.profileName)"
                                :can-reset="hasCapability('admin.scenarios.reset', profile.profileName)"
                                :can-cancel="hasCapability('admin.games.cancel', profile.profileName)"
                            />

                            <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2">
                                <div>
                                    <div class="text-base font-semibold">
                                        {{ adminProfileDisplayName(profile) }}
                                    </div>
                                    <div class="text-xs text-zinc-500">
                                        현재 시나리오: {{ profile.currentScenario ?? '미설정' }}
                                    </div>
                                    <div
                                        class="mt-1 text-xs text-amber-200"
                                        data-testid="profile-lifecycle-description"
                                    >
                                        {{ profileLifecycleText(profile) }}
                                    </div>
                                </div>
                                <div class="text-xs text-zinc-400">
                                    상태: {{ profile.status }} / API: {{ profile.runtime.apiRunning ? 'ON' : 'OFF' }} /
                                    DAEMON: {{ profile.runtime.daemonRunning ? 'ON' : 'OFF' }} / AUCTION:
                                    {{ profile.runtime.auctionRunning ? 'ON' : 'OFF' }} / BATTLE SIM:
                                    {{ profile.runtime.battleSimRunning ? 'ON' : 'OFF' }} / TOURNAMENT:
                                    {{ profile.runtime.tournamentRunning ? 'ON' : 'OFF' }}
                                </div>
                            </div>

                            <div class="text-xs text-zinc-400">빌드 커밋: {{ profile.buildCommitSha ?? '미지정' }}</div>

                            <div
                                v-if="
                                    hasCapability('admin.scenarios.reset', profile.profileName) &&
                                    canCloseCompletedProfile(profile)
                                "
                                class="rounded border border-amber-700/70 bg-amber-950/30 p-3 text-sm text-amber-100"
                                data-testid="completed-profile-cleanup"
                            >
                                <div class="font-semibold">천하통일 서버 정리</div>
                                <p class="mt-1 text-xs text-amber-200/80">
                                    서버를 외부 목록에서 닫고 실행 프로세스를 중지합니다. 현재 기수 DB와 완료 기록은
                                    보존되며, 다음 시나리오 초기화로 다시 열 수 있습니다.
                                </p>
                                <button
                                    class="mt-3 rounded bg-red-700 px-3 py-2 font-semibold text-white hover:bg-red-600 disabled:cursor-not-allowed disabled:opacity-40"
                                    data-testid="completed-profile-cleanup-submit"
                                    :disabled="profileActionSubmitting[profile.profileName]"
                                    @click="requestProfileAction(profile.profileName, 'CLOSE_COMPLETED')"
                                >
                                    완료 서버 닫기
                                </button>
                            </div>

                            <div class="grid md:grid-cols-2 gap-3">
                                <div
                                    v-if="hasCapability('admin.profiles.settings', profile.profileName)"
                                    class="space-y-2"
                                >
                                    <label class="text-xs text-zinc-400">표시명</label>
                                    <input
                                        v-model="profileEdits[profile.profileName].korName"
                                        type="text"
                                        class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                    />
                                    <label class="text-xs text-zinc-400">색상</label>
                                    <input
                                        v-model="profileEdits[profile.profileName].color"
                                        type="text"
                                        class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                    />
                                    <label class="text-xs text-zinc-400">인게임 공지</label>
                                    <textarea
                                        v-model="profileEdits[profile.profileName].inGameNotice"
                                        class="w-full bg-zinc-950 border border-zinc-700 rounded p-2 text-sm text-white min-h-[80px]"
                                    />
                                    <label class="text-xs text-zinc-400">프로필 이미지</label>
                                    <input
                                        v-model="profileEdits[profile.profileName].profileImageUrl"
                                        type="text"
                                        class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                    />
                                    <label class="text-xs text-zinc-400">다음 시즌 번호</label>
                                    <input
                                        v-model="profileEdits[profile.profileName].nextSeasonIdx"
                                        type="number"
                                        min="0"
                                        step="1"
                                        data-testid="next-season-idx"
                                        class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                        placeholder="예: 12"
                                    />
                                    <div class="text-xs text-zinc-500">리셋 시 적용할 시즌 번호를 지정합니다.</div>
                                    <label class="text-xs text-zinc-400">첫 기수 번호</label>
                                    <input
                                        v-model="profileEdits[profile.profileName].firstGameIdx"
                                        type="number"
                                        min="0"
                                        step="1"
                                        data-testid="first-game-idx"
                                        class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                        placeholder="기본값: 1"
                                    />
                                    <div class="text-xs text-zinc-500">
                                        완료된 게임 수에 더할 첫 기수 번호입니다. 다음 리셋부터 적용되며, 완료 이력이
                                        생긴 뒤 바꾸면 이후 기수 번호가 이동합니다.
                                    </div>
                                    <details class="rounded border border-zinc-700 bg-zinc-950/60 p-3">
                                        <summary class="cursor-pointer text-sm font-semibold text-zinc-200">
                                            서버 리셋 기본 옵션
                                        </summary>
                                        <p class="mt-2 text-xs text-zinc-500">
                                            이 서버의 시나리오 초기화 화면을 열 때 자동으로 채울 값을 저장합니다.
                                            시나리오와 예약·오픈 시각은 실행할 때 선택합니다.
                                        </p>
                                        <div class="mt-3 grid gap-3 text-xs sm:grid-cols-2">
                                            <label>
                                                {{ RESET_OPTION_COPY.turnTerm.label }}
                                                <select
                                                    v-model.number="
                                                        profileEdits[profile.profileName].resetDefaults.turnTermMinutes
                                                    "
                                                    class="mt-1 w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-2"
                                                    data-testid="meta-reset-turn-term"
                                                >
                                                    <option
                                                        v-for="minutes in PROFILE_TURN_TERM_MINUTES"
                                                        :key="minutes"
                                                        :value="minutes"
                                                    >
                                                        {{ minutes }}분
                                                    </option>
                                                </select>
                                            </label>
                                            <label>
                                                {{ RESET_OPTION_COPY.joinMode.label }}
                                                <select
                                                    v-model="profileEdits[profile.profileName].resetDefaults.joinMode"
                                                    class="mt-1 w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-2"
                                                >
                                                    <option value="full">일반</option>
                                                    <option value="onlyRandom">랜덤 임관</option>
                                                </select>
                                            </label>
                                            <label>
                                                {{ RESET_OPTION_COPY.fiction.label }}
                                                <select
                                                    v-model.number="
                                                        profileEdits[profile.profileName].resetDefaults.fiction
                                                    "
                                                    class="mt-1 w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-2"
                                                >
                                                    <option :value="0">연의</option>
                                                    <option :value="1">가상</option>
                                                </select>
                                            </label>
                                            <label>
                                                {{ RESET_OPTION_COPY.blockGeneralCreate.label }}
                                                <select
                                                    v-model.number="
                                                        profileEdits[profile.profileName].resetDefaults
                                                            .blockGeneralCreate
                                                    "
                                                    class="mt-1 w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-2"
                                                >
                                                    <option :value="0">가능</option>
                                                    <option :value="2">장수명 무작위</option>
                                                    <option :value="1">불가</option>
                                                </select>
                                            </label>
                                            <label>
                                                {{ RESET_OPTION_COPY.npcMode.label }}
                                                <select
                                                    v-model.number="
                                                        profileEdits[profile.profileName].resetDefaults.npcMode
                                                    "
                                                    class="mt-1 w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-2"
                                                    data-testid="meta-reset-npc-mode"
                                                >
                                                    <option :value="0">불가</option>
                                                    <option :value="1">가능</option>
                                                    <option :value="2">선택 생성 가능</option>
                                                </select>
                                            </label>
                                            <label>
                                                {{ RESET_OPTION_COPY.showImgLevel.label }}
                                                <select
                                                    v-model.number="
                                                        profileEdits[profile.profileName].resetDefaults.showImgLevel
                                                    "
                                                    class="mt-1 w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-2"
                                                >
                                                    <option :value="0">안함</option>
                                                    <option :value="1">전콘</option>
                                                    <option :value="2">전콘, 병종</option>
                                                    <option :value="3">전콘, 병종, NPC</option>
                                                </select>
                                            </label>
                                            <label class="flex items-center gap-2">
                                                <input
                                                    v-model="profileEdits[profile.profileName].resetDefaults.sync"
                                                    type="checkbox"
                                                />
                                                {{ RESET_OPTION_COPY.sync.label }} 사용
                                            </label>
                                            <label class="flex items-center gap-2">
                                                <input
                                                    v-model="profileEdits[profile.profileName].resetDefaults.extend"
                                                    type="checkbox"
                                                />
                                                {{ RESET_OPTION_COPY.extend.label }} 포함
                                            </label>
                                            <label class="flex items-center gap-2">
                                                <input
                                                    v-model="
                                                        profileEdits[profile.profileName].resetDefaults.tournamentTrig
                                                    "
                                                    type="checkbox"
                                                />
                                                {{ RESET_OPTION_COPY.tournamentTrig.label }}
                                            </label>
                                            <label class="flex items-center gap-2">
                                                <input
                                                    v-model="profileEdits[profile.profileName].resetAutorunEnabled"
                                                    type="checkbox"
                                                    @change="ensureResetAutorun(profile.profileName)"
                                                />
                                                {{ RESET_OPTION_COPY.autorun.label }}
                                            </label>
                                            <template
                                                v-if="
                                                    profileEdits[profile.profileName].resetAutorunEnabled &&
                                                    profileEdits[profile.profileName].resetDefaults.autorunUser
                                                "
                                            >
                                                <label>
                                                    {{ RESET_OPTION_COPY.autorunLimit.label }}
                                                    <input
                                                        v-model.number="
                                                            profileEdits[profile.profileName].resetDefaults.autorunUser!
                                                                .limitMinutes
                                                        "
                                                        type="number"
                                                        min="1"
                                                        max="43200"
                                                        class="mt-1 w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-2"
                                                    />
                                                </label>
                                                <div class="flex flex-wrap items-center gap-3 sm:col-span-2">
                                                    <label
                                                        v-for="option in RESET_AUTORUN_LABELS"
                                                        :key="option.value"
                                                        class="flex items-center gap-1"
                                                    >
                                                        <input
                                                            v-model="
                                                                profileEdits[profile.profileName].resetDefaults
                                                                    .autorunUser!.options
                                                            "
                                                            type="checkbox"
                                                            :value="option.value"
                                                        />
                                                        {{ option.label }}
                                                    </label>
                                                </div>
                                            </template>
                                        </div>
                                    </details>
                                    <label class="text-xs text-zinc-400">Kakao 미인증 접근 유예일</label>
                                    <input
                                        v-model="profileEdits[profile.profileName].localAccountAccessGraceDays"
                                        type="number"
                                        min="0"
                                        max="365"
                                        class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                        placeholder="비우면 Gateway 기본값"
                                    />
                                    <label class="text-xs text-zinc-400">Kakao 미인증 장수 생성 유예일</label>
                                    <input
                                        v-model="profileEdits[profile.profileName].localAccountGeneralCreationGraceDays"
                                        type="number"
                                        min="0"
                                        max="365"
                                        class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                        placeholder="비우면 서버 기본값"
                                    />
                                    <div class="text-xs text-zinc-500">
                                        게임 규칙 자체가 아니라 Gateway가 game token을 발급할 때 적용하는 profile별 계정
                                        정책입니다.
                                    </div>
                                    <label class="text-xs text-zinc-400">메타 변경 사유</label>
                                    <input
                                        v-model="profileEdits[profile.profileName].reason"
                                        type="text"
                                        maxlength="200"
                                        class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                        placeholder="변경 사유 (필수)"
                                    />
                                    <button
                                        class="bg-emerald-600 hover:bg-emerald-500 text-black font-semibold px-4 py-2 rounded"
                                        @click="updateProfileMeta(profile.profileName)"
                                    >
                                        메타 저장
                                    </button>
                                </div>

                                <div
                                    v-if="hasCapability('admin.profiles.runtime', profile.profileName)"
                                    class="space-y-2"
                                >
                                    <fieldset
                                        class="space-y-3 rounded border border-zinc-700 bg-zinc-950/60 p-3"
                                        data-testid="runtime-settings"
                                    >
                                        <legend class="px-1 text-sm font-semibold text-zinc-200">
                                            실행 중 게임 옵션
                                        </legend>
                                        <p class="text-xs text-zinc-500">
                                            현재 기수에 즉시 적용합니다. 턴 간격 변경 시 tick 기준 게임 시각은 같은 게임
                                            시각을 유지하도록 재계산하고, 기존 로그 시각은 유지합니다.
                                        </p>
                                        <p v-if="!profile.runtimeSettings" class="text-xs text-amber-400" role="alert">
                                            게임 DB에서 현재 설정을 읽지 못해 변경할 수 없습니다. 실제 처리 상태를
                                            새로고침해 주세요.
                                        </p>
                                        <label class="block text-xs text-zinc-400">
                                            {{ RESET_OPTION_COPY.turnTerm.label }}
                                            <select
                                                v-model.number="profileActions[profile.profileName].turnTermMinutes"
                                                class="mt-1 w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-2 text-sm text-white"
                                                data-testid="runtime-turn-term"
                                            >
                                                <option
                                                    v-for="minutes in PROFILE_TURN_TERM_MINUTES"
                                                    :key="minutes"
                                                    :value="minutes"
                                                >
                                                    {{ minutes }}분
                                                </option>
                                            </select>
                                        </label>
                                        <label class="block text-xs text-zinc-400">
                                            {{ RESET_OPTION_COPY.blockGeneralCreate.label }}
                                            <select
                                                v-model.number="profileActions[profile.profileName].blockGeneralCreate"
                                                class="mt-1 w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-2 text-sm text-white"
                                                data-testid="runtime-block-general-create"
                                            >
                                                <option :value="0">가능</option>
                                                <option :value="2">장수명 무작위</option>
                                                <option :value="1">불가</option>
                                            </select>
                                        </label>
                                        <label class="flex items-center gap-2 text-xs text-zinc-300">
                                            <input
                                                v-model="profileActions[profile.profileName].autorunEnabled"
                                                type="checkbox"
                                                data-testid="runtime-autorun-enabled"
                                            />
                                            {{ RESET_OPTION_COPY.autorun.label }} 사용
                                        </label>
                                        <template v-if="profileActions[profile.profileName].autorunEnabled">
                                            <label class="block text-xs text-zinc-400">
                                                {{ RESET_OPTION_COPY.autorunLimit.label }}
                                                <input
                                                    v-model="profileActions[profile.profileName].autorunUserMinutes"
                                                    type="number"
                                                    min="1"
                                                    max="43200"
                                                    step="1"
                                                    class="mt-1 w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-2 text-sm text-white"
                                                    data-testid="runtime-autorun-minutes"
                                                />
                                            </label>
                                            <div class="flex flex-wrap items-center gap-3 text-xs text-zinc-300">
                                                <label
                                                    v-for="option in RESET_AUTORUN_LABELS"
                                                    :key="`runtime-${option.value}`"
                                                    class="flex items-center gap-1"
                                                >
                                                    <input
                                                        v-model="profileActions[profile.profileName].autorunOptions"
                                                        type="checkbox"
                                                        :value="option.value"
                                                    />
                                                    {{ option.label }}
                                                </label>
                                            </div>
                                        </template>
                                        <button
                                            class="w-full rounded bg-cyan-600 px-4 py-2 font-semibold text-black hover:bg-cyan-500 disabled:cursor-not-allowed disabled:opacity-40"
                                            data-testid="runtime-settings-submit"
                                            :disabled="
                                                profileActionSubmitting[profile.profileName] ||
                                                !profile.runtimeSettings ||
                                                !gatewayProfileCapabilities(profile.status).runtimeExpected ||
                                                !validRuntimeSettings(profile.profileName) ||
                                                runtimeActionPending(profile)
                                            "
                                            @click="
                                                requestProfileAction(profile.profileName, 'UPDATE_RUNTIME_SETTINGS')
                                            "
                                        >
                                            현재 기수에 적용
                                        </button>
                                    </fieldset>
                                    <label class="text-xs text-zinc-400">특수 동작 메모</label>
                                    <input
                                        v-model="profileActions[profile.profileName].reason"
                                        type="text"
                                        class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                        placeholder="사유 / 메모"
                                    />
                                    <label
                                        class="text-xs text-zinc-400"
                                        :for="`runtime-duration-${profile.profileName}`"
                                    >
                                        가속/연기 (분)
                                    </label>
                                    <input
                                        :id="`runtime-duration-${profile.profileName}`"
                                        v-model="profileActions[profile.profileName].durationMinutes"
                                        type="number"
                                        min="1"
                                        max="1440"
                                        step="1"
                                        :aria-describedby="`runtime-duration-help-${profile.profileName}`"
                                        :aria-invalid="
                                            profileActions[profile.profileName].durationMinutes
                                                ? !validDuration(profile.profileName)
                                                : undefined
                                        "
                                        class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-sm text-white"
                                    />
                                    <div
                                        :id="`runtime-duration-help-${profile.profileName}`"
                                        class="text-xs text-zinc-500"
                                    >
                                        1~1440 사이의 정수로 입력해 주세요.
                                    </div>
                                    <div class="grid grid-cols-2 gap-2 pt-2">
                                        <button
                                            class="bg-blue-700 hover:bg-blue-600 text-white font-semibold px-3 py-2 rounded disabled:opacity-40 disabled:cursor-not-allowed"
                                            :disabled="
                                                profileActionSubmitting[profile.profileName] ||
                                                !canResumeProfile(profile)
                                            "
                                            @click="requestProfileAction(profile.profileName, 'RESUME')"
                                        >
                                            {{ profile.status === 'PAUSED' ? '턴 재개' : '서버 재개' }}
                                        </button>
                                        <button
                                            class="bg-zinc-700 hover:bg-zinc-600 text-white font-semibold px-3 py-2 rounded disabled:opacity-40 disabled:cursor-not-allowed"
                                            :disabled="
                                                profileActionSubmitting[profile.profileName] ||
                                                !canPauseProfile(profile)
                                            "
                                            @click="requestProfileAction(profile.profileName, 'PAUSE')"
                                        >
                                            일시정지
                                        </button>
                                        <button
                                            class="bg-red-700 hover:bg-red-600 text-white font-semibold px-3 py-2 rounded disabled:opacity-40 disabled:cursor-not-allowed"
                                            :disabled="
                                                profileActionSubmitting[profile.profileName] || !canStopProfile(profile)
                                            "
                                            @click="requestProfileAction(profile.profileName, 'STOP')"
                                        >
                                            중지
                                        </button>
                                        <button
                                            class="bg-orange-600 hover:bg-orange-500 text-black font-semibold px-3 py-2 rounded disabled:opacity-40 disabled:cursor-not-allowed"
                                            :disabled="
                                                profileActionSubmitting[profile.profileName] ||
                                                !validDuration(profile.profileName) ||
                                                runtimeActionPending(profile)
                                            "
                                            @click="requestProfileAction(profile.profileName, 'ACCELERATE')"
                                        >
                                            가속
                                        </button>
                                        <button
                                            class="bg-yellow-600 hover:bg-yellow-500 text-black font-semibold px-3 py-2 rounded disabled:opacity-40 disabled:cursor-not-allowed"
                                            :disabled="
                                                profileActionSubmitting[profile.profileName] ||
                                                !validDuration(profile.profileName) ||
                                                runtimeActionPending(profile)
                                            "
                                            @click="requestProfileAction(profile.profileName, 'DELAY')"
                                        >
                                            연기
                                        </button>
                                        <button
                                            class="bg-black hover:bg-zinc-800 text-white font-semibold px-3 py-2 rounded"
                                            @click="requestProfileAction(profile.profileName, 'SHUTDOWN')"
                                        >
                                            서버 폐쇄
                                        </button>
                                    </div>
                                    <div class="text-xs text-zinc-500">
                                        {{ profileActionStatus[profile.profileName] }}
                                    </div>
                                    <div
                                        v-if="profile.runtimeActions[0]"
                                        class="rounded border border-zinc-800 bg-zinc-950 p-2 text-xs"
                                        role="status"
                                        aria-live="polite"
                                    >
                                        <div :class="runtimeActionStatusClass(profile.runtimeActions[0].status)">
                                            {{ profile.runtimeActions[0].status }} ·
                                            {{ profile.runtimeActions[0].action }}
                                            {{
                                                profile.runtimeActions[0].durationMinutes
                                                    ? `${profile.runtimeActions[0].durationMinutes}분`
                                                    : ''
                                            }}
                                        </div>
                                        <div v-if="profile.runtimeActions[0].detail" class="mt-1 text-zinc-500">
                                            {{ profile.runtimeActions[0].detail }}
                                        </div>
                                        <div
                                            v-if="
                                                profile.runtimeActions[0].handler || profile.runtimeActions[0].handledAt
                                            "
                                            class="mt-1 text-zinc-600"
                                        >
                                            {{ profile.runtimeActions[0].handler || '처리자 미상' }}
                                            {{
                                                profile.runtimeActions[0].handledAt
                                                    ? ` · ${formatRuntimeActionTime(profile.runtimeActions[0].handledAt)}`
                                                    : ''
                                            }}
                                        </div>
                                    </div>
                                    <button type="button" class="text-xs text-zinc-400 underline" @click="loadProfiles">
                                        실제 처리 상태 새로고침
                                    </button>
                                </div>
                            </div>
                        </div>
                        <div v-if="profileActionStatus.global" class="text-xs text-red-400">
                            {{ profileActionStatus.global }}
                        </div>
                    </div>
                </section>
            </div>
        </div>
    </AdminConsoleLayout>
</template>
