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

import { LogCategory, LogScope } from '@sammo-ts/logic';
import { asRecord } from '@sammo-ts/common';

import type { GameApiContext } from '../../context.js';
import {
    accessEngineAuthedProcedure,
    accessEngineAuthedInputProcedure,
    authedProcedure,
    deferredAccessLimitAuthedProcedure,
    engineAuthedProcedure,
    router,
} from '../../trpc.js';
import { ConflictingTurnDaemonCommandError } from '../../daemon/databaseTransport.js';
import { resolveAccessWindows } from '../../services/generalAccess.js';
import { adjustAccountIconForUser } from '../../services/accountIconSync.js';
import {
    loadCrewTypeDisplayNames,
    loadItemDisplayNames,
    resolveCityLevelName,
    resolveDedicationLevelName,
    resolveNationLevelName,
    resolveOfficerLevelName,
    resolveRegionName,
    sanitizeInternalDisplayCode,
} from '../../services/gameDisplayNames.js';
import { getMyGeneral } from '../shared/general.js';
import {
    loadTraitNames,
    resolveNationBill,
    resolveNationBlockScout,
    resolveNationBlockWar,
    resolveNationNotice,
    resolveNationRate,
    resolveOfficerCity,
    type TraitNameMap,
} from '../nation/shared.js';
import {
    resolveImpossibleStrategicCommands,
    resolveMainNationTech,
    splitNationTraitInfo,
} from '../../services/mainNationProjection.js';
import {
    resolveGeneralTypeCall,
    resolveLeadershipBonus,
    resolveRefreshScoreText,
    resolveRemainingMinutes,
} from '../../services/generalBasicCardProjection.js';

const zGeneralSettings = z.object({
    tnmt: z.number().int().optional(),
    defence_train: z.number().int().optional(),
    use_treatment: z.number().int().optional(),
    use_auto_nation_turn: z.number().int().optional(),
});

const zGeneralLogType = z.enum(['generalHistory', 'battleDetail', 'battleResult', 'generalAction']);
const zImmediateActionInput = z
    .object({
        clientRequestId: z.string().uuid().optional(),
    })
    .optional();
const MAIN_RECORD_LIMIT = 15;
const NEUTRAL_NATION_CONTEXT = {
    id: 0,
    name: '재야',
    color: '#000000',
    level: 0,
    gold: 0,
    rice: 0,
    tech: 0,
    typeCode: 'None',
    capitalCityId: null,
    meta: {},
} as const;

const resolveImmediateActionRequestId = (
    contextRequestId: string | undefined,
    userId: string,
    clientRequestId: string | undefined,
    action: 'buildNationCandidate' | 'dieOnPrestart' | 'instantRetreat'
): string | undefined => {
    if (clientRequestId) {
        return `general:${action}:${userId}:${clientRequestId}`;
    }
    return contextRequestId ? `${contextRequestId}:general.${action}` : undefined;
};

const requestImmediateAction = async (
    ctx: GameApiContext,
    input: { clientRequestId?: string } | undefined,
    action: 'buildNationCandidate' | 'dieOnPrestart' | 'instantRetreat'
): Promise<{ ok: true }> => {
    const userId = ctx.auth?.user.id;
    if (!userId) {
        throw new TRPCError({ code: 'UNAUTHORIZED' });
    }
    const general =
        action === 'dieOnPrestart'
            ? await ctx.db.general.findFirst({ where: { userId, npcState: 0 } })
            : await getMyGeneral(ctx);
    if (!general) {
        throw new TRPCError({ code: 'NOT_FOUND', message: '장수가 없습니다' });
    }
    const requestId = resolveImmediateActionRequestId(ctx.requestId, userId, input?.clientRequestId, action);
    try {
        const result = await ctx.turnDaemon.requestCommand({
            type: action,
            ...(requestId ? { requestId } : {}),
            userId,
            generalId: general.id,
        });
        if (!result) {
            throw new TRPCError({
                code: 'TIMEOUT',
                message: '요청은 접수됐지만 처리 결과를 아직 확인하지 못했습니다. 같은 요청으로 다시 시도해 주세요.',
            });
        }
        if (result.type !== action) {
            throw new TRPCError({
                code: 'INTERNAL_SERVER_ERROR',
                message: '턴 데몬이 올바르지 않은 즉시 행동 결과를 반환했습니다.',
            });
        }
        if (!result.ok) {
            throw new TRPCError({ code: 'BAD_REQUEST', message: result.reason });
        }
        if (action === 'dieOnPrestart' && ctx.accessToken) {
            await ctx.accessTokenStore.revoke(ctx.accessToken);
        }
        return { ok: true };
    } catch (error) {
        if (
            error instanceof ConflictingTurnDaemonCommandError ||
            (error instanceof Error && error.name === 'ConflictingTurnDaemonCommandError')
        ) {
            throw new TRPCError({
                code: 'CONFLICT',
                message: '이미 접수된 즉시 행동 요청과 입력이 다릅니다. 새 요청 번호로 다시 시도해 주세요.',
            });
        }
        throw error;
    }
};

const trimRecentRecords = <Entry extends { id: number }>(entries: Entry[], cursor: number): Entry[] => {
    if (entries.length === 0) {
        return entries;
    }
    const result = [...entries];
    if (result.at(-1)?.id === cursor || result.length > MAIN_RECORD_LIMIT) {
        result.pop();
    }
    return result;
};

const readNumber = (value: unknown, fallback: number): number => {
    if (typeof value === 'number' && Number.isFinite(value)) {
        return value;
    }
    if (typeof value === 'string') {
        const parsed = Number(value);
        if (Number.isFinite(parsed)) {
            return parsed;
        }
    }
    return fallback;
};

const normalizeItemCode = (value: string | null): string | null => {
    if (!value || value === 'None') {
        return null;
    }
    return value;
};

const resolveTraitDisplayName = (code: string, names: TraitNameMap): string => {
    if (!code || code === 'None') {
        return '-';
    }
    const loadedName = names.get(code)?.name;
    if (loadedName) {
        return loadedName;
    }
    // Ref는 class getName()을 표시하므로 로더가 모르는 선택적 특기도 raw namespace는 노출하지 않는다.
    return sanitizeInternalDisplayCode(code);
};

const resolveUserSettings = (meta: Record<string, unknown>) => {
    // The legacy general columns are persisted at the top level of General.meta.
    // Keep reading the short-lived nested shape for installations that ran the
    // initial rewrite implementation before this compatibility fix.
    const nestedSettings = asRecord(meta.userSettings);
    const readSetting = (key: string): unknown => meta[key] ?? nestedSettings[key];
    const mysetRaw = readSetting('myset');
    const myset = typeof mysetRaw === 'number' && Number.isFinite(mysetRaw) ? mysetRaw : null;

    return {
        tnmt: readNumber(readSetting('tnmt'), 1),
        defence_train: readNumber(readSetting('defence_train'), 80),
        use_treatment: readNumber(readSetting('use_treatment'), 10),
        use_auto_nation_turn: readNumber(readSetting('use_auto_nation_turn'), 1),
        myset,
    };
};

const resolvePenalty = (penalty: unknown): Record<string, number> => {
    const penaltyRecord = asRecord(penalty);
    const result: Record<string, number> = {};

    for (const [key, value] of Object.entries(penaltyRecord)) {
        if (typeof value === 'number' && Number.isFinite(value)) {
            result[key] = value;
            continue;
        }
        if (typeof value === 'string') {
            const parsed = Number(value);
            if (Number.isFinite(parsed)) {
                result[key] = parsed;
            }
        }
    }

    return result;
};

export const getGeneralContext = async (ctx: GameApiContext) => {
    const userId = ctx.auth?.user.id;
    if (!userId) {
        throw new TRPCError({ code: 'UNAUTHORIZED' });
    }

    const general = await ctx.db.general.findFirst({
        where: { userId },
        select: {
            id: true,
            name: true,
            npcState: true,
            nationId: true,
            cityId: true,
            troopId: true,
            picture: true,
            imageServer: true,
            leadership: true,
            strength: true,
            intel: true,
            officerLevel: true,
            gold: true,
            rice: true,
            crew: true,
            train: true,
            atmos: true,
            injury: true,
            experience: true,
            dedication: true,
            age: true,
            turnTime: true,
            crewTypeId: true,
            personalCode: true,
            specialCode: true,
            special2Code: true,
            weaponCode: true,
            horseCode: true,
            bookCode: true,
            itemCode: true,
            meta: true,
            penalty: true,
        },
    });

    if (!general) {
        return null;
    }

    const metaRecord = asRecord(general.meta);
    const officerCityId = readNumber(metaRecord.officerCity ?? metaRecord.officer_city ?? metaRecord.officerCityId, 0);
    const [city, queriedNation, worldState, officerCity, troop, troopLeader, troopLeaderFirstTurn, accessLog] =
        await Promise.all([
            general.cityId > 0
                ? ctx.db.city.findUnique({
                      where: { id: general.cityId },
                      select: {
                          id: true,
                          name: true,
                          level: true,
                          nationId: true,
                          population: true,
                          populationMax: true,
                          agriculture: true,
                          agricultureMax: true,
                          commerce: true,
                          commerceMax: true,
                          security: true,
                          securityMax: true,
                          trust: true,
                          trade: true,
                          defence: true,
                          defenceMax: true,
                          wall: true,
                          wallMax: true,
                          region: true,
                          supplyState: true,
                          frontState: true,
                      },
                  })
                : null,
            general.nationId > 0
                ? ctx.db.nation.findUnique({
                      where: { id: general.nationId },
                      select: {
                          id: true,
                          name: true,
                          color: true,
                          level: true,
                          gold: true,
                          rice: true,
                          tech: true,
                          typeCode: true,
                          capitalCityId: true,
                          meta: true,
                      },
                  })
                : Promise.resolve(NEUTRAL_NATION_CONTEXT),
            ctx.db.worldState.findFirst({
                select: { currentYear: true, currentMonth: true, tickSeconds: true, config: true, meta: true },
            }),
            officerCityId > 0
                ? ctx.db.city.findUnique({ where: { id: officerCityId }, select: { name: true } })
                : Promise.resolve(null),
            general.troopId > 0
                ? ctx.db.troop.findUnique({ where: { troopLeaderId: general.troopId }, select: { name: true } })
                : Promise.resolve(null),
            general.troopId > 0
                ? ctx.db.general.findUnique({ where: { id: general.troopId }, select: { cityId: true } })
                : Promise.resolve(null),
            general.troopId > 0
                ? ctx.db.generalTurn.findFirst({
                      where: { generalId: general.troopId },
                      orderBy: { turnIdx: 'asc' },
                      select: { actionCode: true },
                  })
                : Promise.resolve(null),
            ctx.db.generalAccessLog.findUnique({
                where: { generalId: general.id },
                select: { refreshScore: true, refreshScoreTotal: true },
            }),
        ]);
    const nation = queriedNation ?? NEUTRAL_NATION_CONTEXT;

    const [capitalCity, cityNation, troopLeaderCity, nationPopulation, nationCrew, chiefAndCityOfficerRows] =
        await Promise.all([
            nation.capitalCityId
                ? ctx.db.city.findUnique({ where: { id: nation.capitalCityId }, select: { name: true } })
                : Promise.resolve(null),
            city && city.nationId > 0
                ? ctx.db.nation.findUnique({ where: { id: city.nationId }, select: { name: true, color: true } })
                : Promise.resolve(null),
            troopLeader && troopLeader.cityId > 0
                ? ctx.db.city.findUnique({ where: { id: troopLeader.cityId }, select: { name: true } })
                : Promise.resolve(null),
            nation.id > 0
                ? ctx.db.city.aggregate({
                      where: { nationId: nation.id },
                      _count: true,
                      _sum: { population: true, populationMax: true },
                  })
                : Promise.resolve({ _count: 0, _sum: { population: 0, populationMax: 0 } }),
            nation.id > 0
                ? ctx.db.general.aggregate({
                      where: { nationId: nation.id, npcState: { not: 5 } },
                      _count: true,
                      _sum: { crew: true, leadership: true },
                  })
                : Promise.resolve({ _count: 0, _sum: { crew: 0, leadership: 0 } }),
            nation.id > 0 || city
                ? ctx.db.general.findMany({
                      where: {
                          OR: [
                              { nationId: nation.id > 0 ? nation.id : -1, officerLevel: { gte: 11 } },
                              {
                                  officerLevel: { in: [2, 3, 4] },
                                  meta: { path: ['officerCity'], equals: city?.id ?? -1 },
                              },
                              {
                                  officerLevel: { in: [2, 3, 4] },
                                  meta: { path: ['officer_city'], equals: city?.id ?? -1 },
                              },
                          ],
                      },
                      select: { id: true, name: true, npcState: true, nationId: true, officerLevel: true, meta: true },
                      orderBy: { id: 'asc' },
                  })
                : Promise.resolve([]),
        ]);
    const topChiefRows = chiefAndCityOfficerRows.filter(
        (chief) => nation.id > 0 && chief.nationId === nation.id && chief.officerLevel >= 11
    );
    const cityOfficers: Record<2 | 3 | 4, { id: number; name: string; npcState: number } | null> = {
        2: null,
        3: null,
        4: null,
    };
    if (city) {
        for (const officer of chiefAndCityOfficerRows) {
            if (
                (officer.officerLevel === 2 || officer.officerLevel === 3 || officer.officerLevel === 4) &&
                resolveOfficerCity(asRecord(officer.meta)) === city.id
            ) {
                cityOfficers[officer.officerLevel] = {
                    id: officer.id,
                    name: officer.name,
                    npcState: officer.npcState,
                };
            }
        }
    }
    const [personalityNames, domesticNames, warNames, nationTypeNames, crewTypeNames, itemNames] = await Promise.all([
        loadTraitNames([general.personalCode], 'personality'),
        loadTraitNames([general.specialCode], 'domestic'),
        loadTraitNames([general.special2Code], 'war'),
        loadTraitNames([nation.typeCode], 'nation'),
        loadCrewTypeDisplayNames(worldState, ctx.profile.id),
        loadItemDisplayNames([general.horseCode, general.weaponCode, general.bookCode, general.itemCode]),
    ]);

    const worldConfig = asRecord(worldState?.config);
    const constValues = asRecord(worldConfig.const ?? worldConfig.consts);
    const scenarioStat = asRecord(worldConfig.stat);
    const chiefStatMin = readNumber(scenarioStat.chiefMin, 70);
    const statGradeLevel = readNumber(constValues.statGradeLevel, 5);
    const retirementYear = readNumber(constValues.retirementYear, 70);
    const maxDedicationLevel = readNumber(constValues.maxDedLevel, 30);
    const settings = resolveUserSettings(metaRecord);
    const penalties = resolvePenalty(general.penalty);
    const dedicationLevel = readNumber(metaRecord.dedlevel, 0);
    const nationMeta = asRecord(nation.meta);
    const nationType = nationTypeNames.get(nation.typeCode);
    const nationTypeEffects = splitNationTraitInfo(nationType?.info ?? '');
    const nationTech = resolveMainNationTech({
        tech: nation.tech,
        currentYear: worldState?.currentYear ?? 0,
        worldConfig: worldState?.config,
        worldMeta: worldState?.meta,
    });
    const topChiefs = Object.fromEntries(
        topChiefRows.map((chief) => [chief.officerLevel, { id: chief.id, name: chief.name, npcState: chief.npcState }])
    );
    const itemName = (code: string | null): string | null => {
        const normalized = normalizeItemCode(code);
        return normalized ? (itemNames.get(normalized) ?? sanitizeInternalDisplayCode(normalized)) : null;
    };
    const worldMeta = asRecord(worldState?.meta);
    const rawLastExecuted = worldMeta.lastTurnTime ?? worldMeta.turntime;
    const parsedLastExecuted =
        rawLastExecuted instanceof Date
            ? rawLastExecuted
            : typeof rawLastExecuted === 'string'
              ? new Date(rawLastExecuted)
              : null;
    const stats = {
        leadership: general.leadership,
        strength: general.strength,
        intelligence: general.intel,
    };
    const refreshScore = accessLog?.refreshScore ?? 0;
    const refreshScoreTotal = accessLog?.refreshScoreTotal ?? 0;
    const troopStatus: 'inactive' | 'present' | 'away' =
        troopLeaderFirstTurn?.actionCode !== undefined && troopLeaderFirstTurn.actionCode !== 'che_집합'
            ? 'inactive'
            : troopLeader?.cityId === general.cityId
              ? 'present'
              : 'away';

    return {
        general: {
            id: general.id,
            name: general.name,
            npcState: general.npcState,
            nationId: general.nationId,
            cityId: general.cityId,
            troopId: general.troopId,
            picture: general.picture,
            imageServer: general.imageServer,
            officerLevel: general.officerLevel,
            officerLevelText: resolveOfficerLevelName(general.officerLevel, nation.level),
            officerCityName:
                general.officerLevel >= 2 && general.officerLevel <= 4 ? (officerCity?.name ?? null) : null,
            generalType: resolveGeneralTypeCall(stats, chiefStatMin, statGradeLevel),
            leadershipBonus: resolveLeadershipBonus(general.officerLevel, nation.level),
            stats,
            gold: general.gold,
            rice: general.rice,
            crew: general.crew,
            train: general.train,
            atmos: general.atmos,
            injury: general.injury,
            experience: general.experience,
            dedication: general.dedication,
            age: general.age,
            retirementYear,
            turnTime: general.turnTime.toISOString(),
            defenceTrain: settings.defence_train,
            killTurn: readNumber(metaRecord.killturn ?? metaRecord.killTurn, 0),
            remainingMinutes: resolveRemainingMinutes(
                general.turnTime,
                parsedLastExecuted,
                worldState?.tickSeconds ?? 0
            ),
            crewTypeId: general.crewTypeId,
            crewTypeName: crewTypeNames.get(general.crewTypeId) ?? '-',
            traits: {
                personal: resolveTraitDisplayName(general.personalCode, personalityNames),
                specialDomestic: resolveTraitDisplayName(general.specialCode, domesticNames),
                specialWar: resolveTraitDisplayName(general.special2Code, warNames),
            },
            progression: {
                experienceLevel: readNumber(metaRecord.explevel, 0),
                dedicationLevel,
                dedicationText: resolveDedicationLevelName(dedicationLevel, maxDedicationLevel),
                statExperience: {
                    leadership: readNumber(metaRecord.leadership_exp, 0),
                    strength: readNumber(metaRecord.strength_exp, 0),
                    intelligence: readNumber(metaRecord.intel_exp, 0),
                },
                statUpgradeLimit: readNumber(constValues.upgradeLimit, 30),
                dex: [1, 2, 3, 4, 5].map((index) => readNumber(metaRecord[`dex${index}`], 0)),
            },
            items: {
                horse: normalizeItemCode(general.horseCode),
                weapon: normalizeItemCode(general.weaponCode),
                book: normalizeItemCode(general.bookCode),
                item: normalizeItemCode(general.itemCode),
            },
            itemNames: {
                horse: itemName(general.horseCode),
                weapon: itemName(general.weaponCode),
                book: itemName(general.bookCode),
                item: itemName(general.itemCode),
            },
            troop: troop
                ? {
                      name: troop.name,
                      status: troopStatus,
                      leaderCityName: troopLeaderCity?.name ?? null,
                  }
                : null,
            refreshScore: {
                current: refreshScore,
                total: refreshScoreTotal,
                text: resolveRefreshScoreText(refreshScoreTotal),
            },
        },
        iconChoices: ctx.auth?.user.canUseGeneralPicture === false ? [] : (ctx.auth?.user.icons ?? []),
        canChangeIcon: general.npcState === 0 && ctx.auth?.user.canUseGeneralPicture !== false,
        iconChangeAvailableAt:
            typeof metaRecord.generalIconChangedAt === 'string'
                ? new Date(new Date(metaRecord.generalIconChangedAt).getTime() + 24 * 60 * 60 * 1000).toISOString()
                : null,
        city: city
            ? {
                  ...city,
                  levelName: resolveCityLevelName(city.level),
                  regionName: resolveRegionName(city.region),
                  nationName: city.nationId > 0 ? (cityNation?.name ?? '-') : '공백지',
                  nationColor: city.nationId > 0 ? (cityNation?.color ?? '#000000') : '#000000',
                  officers: cityOfficers,
              }
            : null,
        nation: {
            id: nation.id,
            name: nation.name,
            color: nation.color,
            level: nation.level,
            gold: nation.gold,
            rice: nation.rice,
            tech: nation.tech,
            typeCode: nation.typeCode,
            capitalCityId: nation.capitalCityId,
            levelName: resolveNationLevelName(nation.level),
            typeName: nation.id === 0 ? '-' : (nationType?.name ?? sanitizeInternalDisplayCode(nation.typeCode)),
            typePros: nationTypeEffects.pros,
            typeCons: nationTypeEffects.cons,
            capitalCityName: nation.id === 0 ? null : (capitalCity?.name ?? null),
            population: {
                cityCount: nationPopulation._count,
                current: nationPopulation._sum.population ?? 0,
                max: nationPopulation._sum.populationMax ?? 0,
            },
            crew: {
                generalCount: nationCrew._count,
                current: nationCrew._sum.crew ?? 0,
                max: (nationCrew._sum.leadership ?? 0) * 100,
            },
            power: readNumber(nationMeta.power, 0),
            bill: resolveNationBill(nationMeta),
            taxRate: resolveNationRate(nation),
            strategicCommandLimit: readNumber(nationMeta.strategic_cmd_limit, 0),
            diplomaticLimit: readNumber(nationMeta.surlimit, 0),
            prohibitScout: resolveNationBlockScout(nationMeta),
            prohibitWar: resolveNationBlockWar(nationMeta),
            techLevel: nationTech.level,
            techLimited: nationTech.limited,
            topChiefs,
            impossibleStrategicCommands:
                nation.id === 0
                    ? []
                    : resolveImpossibleStrategicCommands(
                          nationMeta,
                          worldState?.currentYear ?? 0,
                          worldState?.currentMonth ?? 1
                      ),
        },
        settings,
        penalties,
    };
};

export const generalRouter = router({
    adjustIcon: engineAuthedProcedure
        .input(
            z.object({ iconId: z.string().uuid().optional(), clientRequestId: z.string().uuid().optional() }).optional()
        )
        .mutation(({ ctx, input }) => {
            const userId = ctx.auth?.user.id;
            if (!userId) {
                throw new TRPCError({ code: 'UNAUTHORIZED' });
            }
            const selected = input?.iconId ? ctx.auth?.user.icons?.find((icon) => icon.id === input.iconId) : undefined;
            if (input?.iconId && (!selected || ctx.auth?.user.canUseGeneralPicture === false)) {
                throw new TRPCError({ code: 'FORBIDDEN', message: '사용 가능한 내 전용 아이콘이 아닙니다.' });
            }
            return adjustAccountIconForUser(
                ctx,
                userId,
                selected
                    ? {
                          picture: selected.picture,
                          imageServer: selected.imageServer,
                          revision: ctx.auth?.user.iconUpdatedAt ?? selected.createdAt,
                      }
                    : undefined,
                true,
                input?.clientRequestId ?? ctx.requestId
            );
        }),
    me: authedProcedure.query(({ ctx }) => getGeneralContext(ctx)),
    ensureDieOnPrestartStatus: accessEngineAuthedProcedure.mutation(async ({ ctx }) => {
        const userId = ctx.auth?.user.id;
        if (!userId) {
            throw new TRPCError({ code: 'UNAUTHORIZED' });
        }
        const general = await ctx.db.general.findFirst({
            where: { userId, npcState: 0 },
            select: { id: true },
        });
        if (!general) {
            return { show: false, available: false, availableAt: null };
        }
        const result = await ctx.turnDaemon.requestCommand({
            type: 'ensureDieOnPrestartStatus',
            ...(ctx.requestId ? { requestId: `${ctx.requestId}:general.ensureDieOnPrestartStatus` } : {}),
            userId,
            generalId: general.id,
        });
        if (!result) {
            throw new TRPCError({
                code: 'TIMEOUT',
                message: '삭제 가능 시각을 아직 확인하지 못했습니다. 다시 시도해 주세요.',
            });
        }
        if (result.type !== 'ensureDieOnPrestartStatus') {
            throw new TRPCError({
                code: 'INTERNAL_SERVER_ERROR',
                message: '턴 데몬이 올바르지 않은 삭제 상태를 반환했습니다.',
            });
        }
        return {
            show: result.show,
            available: result.available,
            availableAt: result.availableAt ?? null,
        };
    }),
    dieOnPrestart: accessEngineAuthedInputProcedure(zImmediateActionInput).mutation(({ ctx, input }) =>
        requestImmediateAction(ctx, input, 'dieOnPrestart')
    ),
    buildNationCandidate: accessEngineAuthedInputProcedure(zImmediateActionInput).mutation(({ ctx, input }) =>
        requestImmediateAction(ctx, input, 'buildNationCandidate')
    ),
    instantRetreat: accessEngineAuthedInputProcedure(zImmediateActionInput).mutation(({ ctx, input }) =>
        requestImmediateAction(ctx, input, 'instantRetreat')
    ),
    vacation: engineAuthedProcedure.mutation(async ({ ctx }) => {
        const general = await getMyGeneral(ctx);
        const result = await ctx.turnDaemon.requestCommand({
            type: 'vacation',
            ...(ctx.requestId ? { requestId: `${ctx.requestId}:general.vacation:engine:0:vacation` } : {}),
            generalId: general.id,
        });
        if (!result || result.type !== 'vacation') {
            throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
        }
        if (!result.ok) {
            throw new TRPCError({ code: 'BAD_REQUEST', message: result.reason });
        }
        return { ok: true };
    }),
    setMySetting: accessEngineAuthedInputProcedure(zGeneralSettings).mutation(async ({ ctx, input }) => {
        const general = await getMyGeneral(ctx);
        const result = await ctx.turnDaemon.requestCommand({
            type: 'setMySetting',
            ...(ctx.requestId
                ? { requestId: `${ctx.requestId}:general.setMySetting:engine:0:setMySetting` }
                : {}),
            generalId: general.id,
            settings: input,
        });
        if (!result || result.type !== 'setMySetting') {
            throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
        }
        if (!result.ok) {
            throw new TRPCError({ code: 'BAD_REQUEST', message: result.reason });
        }

        return { ok: true };
    }),
    dropItem: engineAuthedProcedure.input(z.object({ itemType: z.string() })).mutation(async ({ ctx, input }) => {
        const general = await getMyGeneral(ctx);
        const result = await ctx.turnDaemon.requestCommand({
            type: 'dropItem',
            ...(ctx.requestId ? { requestId: `${ctx.requestId}:general.dropItem:engine:0:dropItem` } : {}),
            generalId: general.id,
            itemType: input.itemType,
        });
        if (!result || result.type !== 'dropItem') {
            throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
        }
        if (!result.ok) {
            throw new TRPCError({ code: 'BAD_REQUEST', message: result.reason });
        }
        return { ok: true };
    }),
    getMyLog: authedProcedure
        .input(
            z.object({
                type: zGeneralLogType,
                beforeId: z.number().int().positive().optional(),
            })
        )
        .query(async ({ ctx, input }) => {
            const me = await getMyGeneral(ctx);

            const categoryMap: Record<z.infer<typeof zGeneralLogType>, LogCategory> = {
                generalHistory: LogCategory.HISTORY,
                generalAction: LogCategory.ACTION,
                battleResult: LogCategory.BATTLE_BRIEF,
                battleDetail: LogCategory.BATTLE_DETAIL,
            };

            const logs = await ctx.db.logEntry.findMany({
                where: {
                    generalId: me.id,
                    scope: LogScope.GENERAL,
                    category: categoryMap[input.type],
                    ...(input.type !== 'generalHistory' && input.beforeId ? { id: { lt: input.beforeId } } : {}),
                },
                orderBy: { id: 'desc' },
                ...(input.type === 'generalHistory' ? {} : { take: 24 }),
            });

            return {
                type: input.type,
                logs: logs.map((entry) => ({
                    id: entry.id,
                    text: entry.text,
                })),
            };
        }),
    getRecentRecords: deferredAccessLimitAuthedProcedure
        .input(
            z.object({
                lastGeneralRecordId: z.number().int().nonnegative().default(0),
                lastWorldHistoryId: z.number().int().nonnegative().default(0),
            })
        )
        .query(async ({ ctx, input }) => {
            const me = await getMyGeneral(ctx);
            const take = MAIN_RECORD_LIMIT + 1;
            const [global, general, history] = await Promise.all([
                ctx.db.logEntry.findMany({
                    where: {
                        scope: LogScope.SYSTEM,
                        category: { in: [LogCategory.SUMMARY, LogCategory.ACTION] },
                        id: { gte: input.lastGeneralRecordId },
                    },
                    orderBy: { id: 'desc' },
                    take,
                    select: { id: true, text: true, createdAt: true },
                }),
                ctx.db.logEntry.findMany({
                    where: {
                        scope: LogScope.GENERAL,
                        category: LogCategory.ACTION,
                        generalId: me.id,
                        id: { gte: input.lastGeneralRecordId },
                    },
                    orderBy: { id: 'desc' },
                    take,
                    select: { id: true, text: true, createdAt: true },
                }),
                ctx.db.logEntry.findMany({
                    where: {
                        scope: LogScope.SYSTEM,
                        category: LogCategory.HISTORY,
                        id: { gte: input.lastWorldHistoryId },
                    },
                    orderBy: { id: 'desc' },
                    take,
                    select: { id: true, text: true, createdAt: true },
                }),
            ]);

            return {
                global: trimRecentRecords(global, input.lastGeneralRecordId),
                general: trimRecentRecords(general, input.lastGeneralRecordId),
                history: trimRecentRecords(history, input.lastWorldHistoryId),
            };
        }),
    // 메인 화면은 SSE invalidation, 탭 복귀와 직접 갱신이 같은 read model을
    // 호출한다. 클라이언트가 주장하는 갱신 원인을 신뢰해 구분하지 않고 이
    // projection 전체를 무가점으로 두되, 이미 제한된 사용자의 gate는 유지한다.
    getFrontStatus: deferredAccessLimitAuthedProcedure.query(async ({ ctx }) => {
        const me = await getMyGeneral(ctx);
        const worldState = await ctx.db.worldState.findFirst({
            orderBy: { id: 'asc' },
            select: {
                tickSeconds: true,
                meta: true,
            },
        });
        if (!worldState) {
            throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'World state is not initialized.' });
        }

        const now = new Date();
        const { scoreStartedAt } = resolveAccessWindows(now, worldState.tickSeconds, worldState.meta);
        const [onlineAccess, ownNation, latestVote] = await Promise.all([
            ctx.db.generalAccessLog.findMany({
                where: {
                    lastActionAt: {
                        gte: scoreStartedAt,
                    },
                },
                select: { generalId: true },
            }),
            me.nationId > 0
                ? ctx.db.nation.findUnique({
                      where: { id: me.nationId },
                      select: { meta: true },
                  })
                : Promise.resolve(null),
            ctx.db.votePoll.findFirst({
                where: {
                    startAt: { lte: now },
                    closedAt: null,
                    OR: [{ endAt: null }, { endAt: { gte: now } }],
                },
                orderBy: { id: 'desc' },
                select: {
                    id: true,
                    title: true,
                },
            }),
        ]);

        const onlineGeneralIds = onlineAccess.map((entry) => entry.generalId);
        const onlineGenerals =
            onlineGeneralIds.length > 0
                ? await ctx.db.general.findMany({
                      where: { id: { in: onlineGeneralIds } },
                      orderBy: { id: 'asc' },
                      select: {
                          id: true,
                          name: true,
                          nationId: true,
                      },
                  })
                : [];
        const nationIds = [...new Set(onlineGenerals.map((general) => general.nationId).filter((id) => id > 0))];
        const nations =
            nationIds.length > 0
                ? await ctx.db.nation.findMany({
                      where: { id: { in: nationIds } },
                      select: {
                          id: true,
                          name: true,
                      },
                  })
                : [];
        const nationNames = new Map(nations.map((nation) => [nation.id, nation.name]));
        const onlineByNation = new Map<number, typeof onlineGenerals>();
        for (const general of onlineGenerals) {
            const bucket = onlineByNation.get(general.nationId) ?? [];
            bucket.push(general);
            onlineByNation.set(general.nationId, bucket);
        }
        const onlineNations = [...onlineByNation.entries()]
            .filter(([nationId]) => nationId > 0)
            .sort((left, right) => right[1].length - left[1].length || left[0] - right[0])
            .map(([nationId]) => `【${nationNames.get(nationId) ?? `세력 ${nationId}`}】`)
            .join(', ');
        const myOnlineGenerals = onlineGenerals
            .filter((general) => general.nationId === me.nationId)
            .map((general) => general.name)
            .join(', ');
        const myVote = latestVote
            ? await ctx.db.vote.findFirst({
                  where: {
                      voteId: latestVote.id,
                      generalId: me.id,
                  },
                  select: { id: true },
              })
            : null;
        const worldMeta = asRecord(worldState.meta);
        const serverId =
            typeof worldMeta.serverId === 'string' && worldMeta.serverId.trim()
                ? worldMeta.serverId.trim()
                : ctx.profile?.name || 'game';
        const rawLastExecuted = worldMeta.lastTurnTime ?? worldMeta.turntime;
        const parsedLastExecuted =
            typeof rawLastExecuted === 'string' || rawLastExecuted instanceof Date ? new Date(rawLastExecuted) : null;

        return {
            serverId,
            onlineUserCount: onlineGenerals.length,
            onlineNations,
            onlineGenerals: myOnlineGenerals,
            nationNotice: ownNation ? resolveNationNotice(asRecord(ownNation.meta)) : '',
            lastExecuted:
                parsedLastExecuted && Number.isFinite(parsedLastExecuted.getTime())
                    ? parsedLastExecuted.toISOString()
                    : null,
            latestVote: latestVote
                ? {
                      id: latestVote.id,
                      title: latestVote.title,
                      hasVoted: Boolean(myVote),
                  }
                : null,
        };
    }),
});
