import { createHmac, randomUUID } from 'node:crypto';
import { z } from 'zod';
import { env } from '../config/env.js';
import { db } from '../db/client.js';
import {
  assignSharedResourceTokenToRequests,
  listChatConversationRequestUsageBySharedResourceTokenIds,
  listChatConversationRequests,
} from './chat-room-service.js';
import { getTokenSettingById, getTokenSettingsConfig, type TokenSettingRecord } from './token-setting-config-service.js';
import type { RequestAuditContext } from '../utils/request-audit.js';

const SHARED_RESOURCE_TOKENS_TABLE = 'shared_resource_tokens';
const SHARED_RESOURCE_TOKEN_ACTIVITIES_TABLE = 'shared_resource_token_activities';
const SHARED_RESOURCE_ACCESS_PIN_SESSIONS_TABLE = 'shared_resource_access_pin_sessions';
const UNBOUNDED_NUMERIC_LIMIT = Number.MAX_SAFE_INTEGER;
const CHAT_SHARE_TOKEN_VERSION = 1;

const sharedResourcePermissionSchema = z.enum(['view', 'download', 'comment', 'upload', 'manage']);
const sharedResourceTypeSchema = z.enum(['file', 'directory', 'document', 'chat-share', 'external-url']);

export const sharedResourceTokenSchema = z.object({
  id: z.string().trim().min(1).max(120).optional(),
  name: z.string().trim().min(1).max(120).optional(),
  title: z.string().trim().min(1).max(120).optional(),
  description: z.string().trim().max(2000).optional().nullable(),
  tokenSettingId: z.string().trim().min(1).max(120).optional().nullable(),
  resourceLabel: z.string().trim().min(1).max(160).optional(),
  resourcePath: z.string().trim().min(1).max(2000),
  resourceType: sharedResourceTypeSchema.default('directory'),
  shareToken: z.string().trim().max(255).optional().nullable(),
  sharePath: z.string().trim().max(2000).optional().nullable(),
  resourceAllowedAppIds: z.array(z.string().trim().min(1).max(120)).max(100).optional().nullable(),
  resourceAllowedAppIdsOverrideEnabled: z.boolean().optional(),
  permissions: z.array(sharedResourcePermissionSchema).default(['view']),
  enabled: z.boolean().default(true),
  expiresAt: z.string().trim().datetime({ offset: true }).optional().nullable(),
  usageLimit: z.number().int().min(0).max(UNBOUNDED_NUMERIC_LIMIT).default(0),
  accessPin: z.string().regex(/^\d{4}$/u).optional().nullable(),
  accessPinPromptTtlMinutes: z.number().int().min(0).max(7 * 24 * 60).optional().nullable(),
  allowAccessPinChangeWithoutManage: z.boolean().optional(),
  tokenSettingSnapshot: z.object({
    id: z.string().trim().min(1).max(120),
    name: z.string().trim().min(1).max(120),
    defaultExpiresInMinutes: z.number().int().min(0).max(5256000).default(0),
    maxTokensPer30Days: z.number().int().min(0).max(UNBOUNDED_NUMERIC_LIMIT).default(0),
    maxTokensPer7Days: z.number().int().min(0).max(UNBOUNDED_NUMERIC_LIMIT).default(0),
    maxTokensPer5Hours: z.number().int().min(0).max(UNBOUNDED_NUMERIC_LIMIT).default(0),
    oneTimeTokenLimit: z.number().int().min(0).max(UNBOUNDED_NUMERIC_LIMIT).default(0),
    allowedAppIds: z.array(z.string().trim().min(1).max(120)).max(100).default([]),
  }).optional().nullable(),
  resourceContext: z.object({
    kind: z.enum(['request-bundle', 'inquiry-message', 'prompt']).optional().nullable(),
    sessionId: z.string().trim().min(1).max(255).optional().nullable(),
    requestId: z.string().trim().min(1).max(255).optional().nullable(),
    sourceMessageId: z.number().int().positive().optional().nullable(),
    promptIndex: z.number().int().min(0).max(99).optional().nullable(),
    promptSignature: z.string().trim().min(1).max(8000).optional().nullable(),
  }).optional().nullable(),
});

const sharedResourceActivityInputSchema = z.object({
  type: z.enum(['created', 'updated', 'permission-granted', 'permission-revoked', 'revoked', 'restored', 'deleted', 'usage']),
  actorLabel: z.string().trim().max(120).optional().nullable(),
  summary: z.string().trim().min(1).max(400),
  detail: z.string().trim().max(4000).optional().nullable(),
  usageDelta: z.number().int().min(0).max(1_000_000).optional().nullable(),
});

export type SharedResourcePermission = z.infer<typeof sharedResourcePermissionSchema>;
export type SharedResourceType = z.infer<typeof sharedResourceTypeSchema>;
export type SharedResourceTokenInput = z.infer<typeof sharedResourceTokenSchema>;
export type SharedResourceActivityInput = z.infer<typeof sharedResourceActivityInputSchema>;

export type SharedResourceTokenSettingSnapshot = NonNullable<z.infer<typeof sharedResourceTokenSchema.shape.tokenSettingSnapshot>>;
export type SharedResourceContext = NonNullable<z.infer<typeof sharedResourceTokenSchema.shape.resourceContext>>;

export type SharedResourceTokenRecord = {
  id: string;
  name: string;
  description: string;
  tokenSettingId: string | null;
  tokenSettingSnapshot: SharedResourceTokenSettingSnapshot | null;
  resourceContext: SharedResourceContext | null;
  linkedTokenSetting: SharedResourceLinkedTokenSettingSummary | null;
  resourceLabel: string;
  resourcePath: string;
  resourceType: SharedResourceType;
  shareToken: string;
  sharePath: string;
  resourceAllowedAppIds: string[];
  resourceAllowedAppIdsOverrideEnabled: boolean;
  allowedAppIds: string[];
  permissions: SharedResourcePermission[];
  enabled: boolean;
  revokedAt: string | null;
  deletedAt: string | null;
  expiresAt: string | null;
  effectiveExpiresAt: string | null;
  usageLimit: number;
  usageCount: number;
  hasAccessPin: boolean;
  accessPinPromptTtlMinutes: number | null;
  allowAccessPinChangeWithoutManage: boolean;
  usageTokenTotal: number;
  usageRequestCount: number;
  usageCompletedRequestCount: number;
  lastUsedAt: string | null;
  lastTokenUsedAt: string | null;
  createdAt: string;
  updatedAt: string;
  lastActivityAt: string | null;
  activityCount: number;
};

export type SharedResourceLinkedTokenSettingSummary = {
  id: string;
  name: string;
  enabled: boolean;
  defaultExpiresInMinutes: number;
  allowedAppIds: string[];
  syncState: 'ok' | 'missing' | 'disabled' | 'chat-share-disallowed';
  syncMessage: string | null;
};

export type SharedResourceTokenActivityRecord = {
  id: number;
  tokenId: string;
  type: SharedResourceActivityInput['type'];
  actorLabel: string | null;
  summary: string;
  detail: string | null;
  usageDelta: number;
  clientIp: string | null;
  externalIp: string | null;
  forwardedFor: string | null;
  realIp: string | null;
  host: string | null;
  origin: string | null;
  referer: string | null;
  userAgent: string | null;
  clientId: string | null;
  createdAt: string;
};

export type SharedResourceTokenBulkActionResult = {
  requestedTokenIds: string[];
  processedTokenIds: string[];
  skippedTokenIds: string[];
  missingTokenIds: string[];
};

type ChatShareKind = 'request-bundle' | 'inquiry-message' | 'prompt';

type ChatShareTokenPayload = {
  version: number;
  kind: ChatShareKind;
  sessionId: string;
  requestId: string;
  tokenSettingId?: string;
  tokenSettingName?: string;
  tokenSettingDefaultExpiresInMinutes?: number;
  tokenSettingAllowedAppIds?: string[];
  tokenSettingMaxTokensPer30Days?: number;
  tokenSettingMaxTokensPer7Days?: number;
  tokenSettingMaxTokensPer5Hours?: number;
  tokenSettingOneTimeTokenLimit?: number;
  managedResourceTokenId?: string;
  sourceMessageId?: number;
  promptIndex?: number;
  promptSignature?: string;
};

function normalizeText(value: unknown) {
  return typeof value === 'string' ? value.trim() : '';
}

function normalizeOptionalText(value: unknown) {
  const normalized = normalizeText(value);
  return normalized || null;
}

function normalizeStringArray(value: unknown) {
  if (!Array.isArray(value)) {
    return [];
  }

  return Array.from(
    new Set(
      value
        .filter((item): item is string => typeof item === 'string')
        .map((item) => item.trim())
        .filter(Boolean),
    ),
  ).sort((left, right) => left.localeCompare(right, 'en'));
}

function normalizePositiveInteger(value: unknown, fallback: number, min: number, max: number) {
  const resolved = typeof value === 'number' ? value : Number(value);

  if (!Number.isFinite(resolved)) {
    return fallback;
  }

  return Math.min(max, Math.max(min, Math.round(resolved)));
}

function normalizeNullablePositiveInteger(value: unknown, min: number, max: number) {
  if (value == null || value === '') {
    return null;
  }

  const resolved = typeof value === 'number' ? value : Number(value);

  if (!Number.isFinite(resolved)) {
    return null;
  }

  return Math.min(max, Math.max(min, Math.round(resolved)));
}

function normalizeBoolean(value: unknown, fallback = true) {
  if (typeof value === 'boolean') {
    return value;
  }

  if (typeof value === 'number') {
    return value !== 0;
  }

  if (typeof value === 'string') {
    const normalized = value.trim().toLowerCase();
    if (['false', '0', 'off', 'n', 'no'].includes(normalized)) {
      return false;
    }
    if (['true', '1', 'on', 'y', 'yes'].includes(normalized)) {
      return true;
    }
  }

  return fallback;
}

function normalizePermissions(value: unknown): SharedResourcePermission[] {
  const parsed = z.array(sharedResourcePermissionSchema).safeParse(Array.isArray(value) ? value : []);

  if (!parsed.success) {
    return ['view'];
  }

  const items = Array.from(new Set(parsed.data));
  return items.length > 0 ? items : ['view'];
}

function normalizeResourceType(value: unknown): SharedResourceType {
  const parsed = sharedResourceTypeSchema.safeParse(value);
  return parsed.success ? parsed.data : 'directory';
}

function normalizeDateTime(value: unknown) {
  if (value instanceof Date) {
    const timestamp = value.getTime();
    return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
  }

  if (typeof value === 'number') {
    return Number.isFinite(value) ? new Date(value).toISOString() : null;
  }

  const normalized = normalizeText(value);

  if (!normalized) {
    return null;
  }

  const timestamp = Date.parse(normalized);
  return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
}

function normalizeTokenSettingSnapshot(value: unknown): SharedResourceTokenSettingSnapshot | null {
  const parsed = sharedResourceTokenSchema.shape.tokenSettingSnapshot.safeParse(value);
  return parsed.success ? parsed.data ?? null : null;
}

function normalizeResourceContext(value: unknown): SharedResourceContext | null {
  const parsed = sharedResourceTokenSchema.shape.resourceContext.safeParse(value);

  if (!parsed.success || !parsed.data) {
    return null;
  }

  const sessionId = normalizeText(parsed.data.sessionId);
  const requestId = normalizeText(parsed.data.requestId);

  if (!sessionId || !requestId) {
    return null;
  }

  return {
    kind: (parsed.data.kind ?? 'request-bundle') as ChatShareKind,
    sessionId,
    requestId,
    sourceMessageId: typeof parsed.data.sourceMessageId === 'number' ? parsed.data.sourceMessageId : null,
    promptIndex: typeof parsed.data.promptIndex === 'number' ? parsed.data.promptIndex : null,
    promptSignature: normalizeOptionalText(parsed.data.promptSignature),
  };
}

function buildTokenSettingSnapshotFromRecord(setting: Pick<
  TokenSettingRecord,
  'id' | 'name' | 'defaultExpiresInMinutes' | 'maxTokensPer30Days' | 'maxTokensPer7Days' | 'maxTokensPer5Hours' | 'oneTimeTokenLimit' | 'allowedAppIds'
>): SharedResourceTokenSettingSnapshot {
  return {
    id: normalizeText(setting.id),
    name: normalizeText(setting.name),
    defaultExpiresInMinutes: Math.max(0, Math.round(Number(setting.defaultExpiresInMinutes ?? 0))),
    maxTokensPer30Days: Math.max(0, Math.round(Number(setting.maxTokensPer30Days ?? 0))),
    maxTokensPer7Days: Math.max(0, Math.round(Number(setting.maxTokensPer7Days ?? 0))),
    maxTokensPer5Hours: Math.max(0, Math.round(Number(setting.maxTokensPer5Hours ?? 0))),
    oneTimeTokenLimit: Math.max(0, Math.round(Number(setting.oneTimeTokenLimit ?? 0))),
    allowedAppIds: normalizeStringArray(setting.allowedAppIds),
  };
}

function createShareToken() {
  return randomUUID().replace(/-/g, '').slice(0, 24);
}

function isSharedResourceShareTokenUniqueError(error: unknown) {
  const dbError = error as { code?: unknown; constraint?: unknown; message?: unknown } | null;
  const code = normalizeText(dbError?.code);
  const constraint = normalizeText(dbError?.constraint);
  const message = normalizeText(dbError?.message).toLowerCase();

  if (constraint === 'shared_resource_tokens_share_token_unique') {
    return true;
  }

  if (code === '23505' && message.includes('share_token')) {
    return true;
  }

  return message.includes('shared_resource_tokens_share_token_unique')
    || (message.includes('unique constraint') && message.includes('share_token'))
    || (message.includes('duplicate key value') && message.includes('share_token'))
    || (message.includes('duplicate entry') && message.includes('share_token'))
    || (message.includes('sqlite_constraint') && message.includes('share_token'));
}

function hashAccessPin(shareToken: string, pin: string) {
  return createHmac('sha256', env.SERVER_COMMAND_ACCESS_TOKEN)
    .update(`${normalizeText(shareToken)}:${normalizeText(pin)}`)
    .digest('hex');
}

function decodeBase64Url(value: string) {
  return Buffer.from(value, 'base64url').toString('utf-8');
}

function signChatSharePayload(encodedPayload: string) {
  return createHmac('sha256', env.SERVER_COMMAND_ACCESS_TOKEN).update(encodedPayload).digest('base64url');
}

export function parseChatShareTokenFromPath(sharePath: string): ChatShareTokenPayload | null {
  const normalizedPath = normalizeText(sharePath);
  const prefix = '/chat/share/';

  if (!normalizedPath.startsWith(prefix)) {
    return null;
  }

  const encodedToken = normalizedPath.slice(prefix.length).trim();

  if (!encodedToken) {
    return null;
  }

  const token = decodeURIComponent(encodedToken);
  const delimiterIndex = token.lastIndexOf('.');

  if (delimiterIndex <= 0 || delimiterIndex >= token.length - 1) {
    return null;
  }

  const encodedPayload = token.slice(0, delimiterIndex);
  const signature = token.slice(delimiterIndex + 1);
  const expectedSignature = signChatSharePayload(encodedPayload);

  if (signature !== expectedSignature) {
    return null;
  }

  try {
    const parsed = JSON.parse(decodeBase64Url(encodedPayload)) as Partial<ChatShareTokenPayload>;

    if (
      parsed.version !== CHAT_SHARE_TOKEN_VERSION ||
      (parsed.kind !== 'request-bundle' && parsed.kind !== 'inquiry-message' && parsed.kind !== 'prompt') ||
      typeof parsed.sessionId !== 'string' ||
      typeof parsed.requestId !== 'string'
    ) {
      return null;
    }

    return {
      version: CHAT_SHARE_TOKEN_VERSION,
      kind: parsed.kind,
      sessionId: parsed.sessionId.trim(),
      requestId: parsed.requestId.trim(),
      tokenSettingId: typeof parsed.tokenSettingId === 'string' ? parsed.tokenSettingId.trim() : undefined,
      tokenSettingName: typeof parsed.tokenSettingName === 'string' ? parsed.tokenSettingName.trim() : undefined,
      tokenSettingDefaultExpiresInMinutes:
        typeof parsed.tokenSettingDefaultExpiresInMinutes === 'number' ? parsed.tokenSettingDefaultExpiresInMinutes : undefined,
      tokenSettingAllowedAppIds: normalizeStringArray(parsed.tokenSettingAllowedAppIds),
      tokenSettingMaxTokensPer30Days:
        typeof parsed.tokenSettingMaxTokensPer30Days === 'number' ? parsed.tokenSettingMaxTokensPer30Days : undefined,
      tokenSettingMaxTokensPer7Days:
        typeof parsed.tokenSettingMaxTokensPer7Days === 'number' ? parsed.tokenSettingMaxTokensPer7Days : undefined,
      tokenSettingMaxTokensPer5Hours:
        typeof parsed.tokenSettingMaxTokensPer5Hours === 'number' ? parsed.tokenSettingMaxTokensPer5Hours : undefined,
      tokenSettingOneTimeTokenLimit:
        typeof parsed.tokenSettingOneTimeTokenLimit === 'number' ? parsed.tokenSettingOneTimeTokenLimit : undefined,
      managedResourceTokenId: typeof parsed.managedResourceTokenId === 'string' ? parsed.managedResourceTokenId.trim() : undefined,
      sourceMessageId: typeof parsed.sourceMessageId === 'number' ? parsed.sourceMessageId : undefined,
      promptIndex: typeof parsed.promptIndex === 'number' ? parsed.promptIndex : undefined,
      promptSignature: typeof parsed.promptSignature === 'string' ? parsed.promptSignature.trim() : undefined,
    };
  } catch {
    return null;
  }
}

function inferDefaultExpiresInMinutes(createdAt: unknown, expiresAt: unknown) {
  const createdAtMs = Date.parse(normalizeText(createdAt));
  const expiresAtMs = Date.parse(normalizeText(expiresAt));

  if (!Number.isFinite(createdAtMs) || !Number.isFinite(expiresAtMs) || expiresAtMs <= createdAtMs) {
    return 0;
  }

  return Math.max(0, Math.round((expiresAtMs - createdAtMs) / 60000));
}

function buildChatShareTokenSettingSnapshotFromPayload(
  payload: ChatShareTokenPayload | null,
  options?: {
    createdAt?: unknown;
    expiresAt?: unknown;
    fallbackTokenSettingId?: unknown;
  },
): SharedResourceTokenSettingSnapshot | null {
  const snapshotId = normalizeText(payload?.tokenSettingId) || normalizeText(options?.fallbackTokenSettingId);
  const snapshotName = normalizeText(payload?.tokenSettingName) || snapshotId;

  if (!snapshotId || !snapshotName) {
    return null;
  }

  return {
    id: snapshotId,
    name: snapshotName,
    defaultExpiresInMinutes:
      typeof payload?.tokenSettingDefaultExpiresInMinutes === 'number'
        ? Math.max(0, Math.round(payload.tokenSettingDefaultExpiresInMinutes))
        : inferDefaultExpiresInMinutes(options?.createdAt, options?.expiresAt),
    maxTokensPer30Days: Math.max(0, Math.round(payload?.tokenSettingMaxTokensPer30Days ?? 0)),
    maxTokensPer7Days: Math.max(0, Math.round(payload?.tokenSettingMaxTokensPer7Days ?? 0)),
    maxTokensPer5Hours: Math.max(0, Math.round(payload?.tokenSettingMaxTokensPer5Hours ?? 0)),
    oneTimeTokenLimit: Math.max(0, Math.round(payload?.tokenSettingOneTimeTokenLimit ?? 0)),
    allowedAppIds: normalizeStringArray(payload?.tokenSettingAllowedAppIds),
  };
}

type LegacyChatShareMigrationRow = {
  token_setting_id?: unknown;
  token_setting_snapshot_json?: unknown;
  resource_context_json?: unknown;
  allowed_app_ids_json?: unknown;
};

function parseAllowedAppIdsJson(value: unknown) {
  if (typeof value !== 'string') {
    return [];
  }

  try {
    return normalizeStringArray(JSON.parse(value));
  } catch {
    return [];
  }
}

export function isLegacyChatShareTokenRowNeedingMigration(row: LegacyChatShareMigrationRow) {
  if (normalizeText(row.token_setting_id)) {
    return true;
  }

  if (normalizeTokenSettingSnapshot(row.token_setting_snapshot_json) == null) {
    return true;
  }

  if (normalizeResourceContext(row.resource_context_json) == null) {
    return true;
  }

  return false;
}

function resolveRequestUsageAtMs(request: Awaited<ReturnType<typeof listChatConversationRequests>>[number]) {
  const candidates = [request.answeredAt, request.terminalAt, request.updatedAt, request.createdAt];

  for (const candidate of candidates) {
    const normalized = normalizeText(candidate);

    if (!normalized) {
      continue;
    }

    const parsed = Date.parse(normalized);

    if (Number.isFinite(parsed)) {
      return parsed;
    }
  }

  return 0;
}

function resolveRequestUsageTotalTokens(request: Awaited<ReturnType<typeof listChatConversationRequests>>[number]) {
  const total =
    request.usageSnapshot?.tokenTotals?.total ??
    request.usageSnapshot?.totalTokens ??
    request.totalTokens ??
    0;

  return Math.max(0, Math.round(Number(total) || 0));
}

function resolveShareRootRequestId(
  requestId: string,
  requestMap: Map<string, Awaited<ReturnType<typeof listChatConversationRequests>>[number]>,
  kind: ChatShareKind,
) {
  const normalizedRequestId = normalizeText(requestId);

  if (!normalizedRequestId || kind !== 'request-bundle') {
    return normalizedRequestId;
  }

  const visitedRequestIds = new Set<string>();
  let currentRequestId = normalizedRequestId;

  while (currentRequestId && !visitedRequestIds.has(currentRequestId)) {
    visitedRequestIds.add(currentRequestId);
    const currentRequest = requestMap.get(currentRequestId);
    const parentRequestId = normalizeText(currentRequest?.parentRequestId);

    if (!parentRequestId || !requestMap.has(parentRequestId)) {
      break;
    }

    currentRequestId = parentRequestId;
  }

  return currentRequestId;
}

function buildChildRequestIdsByParentRequestId(requests: Awaited<ReturnType<typeof listChatConversationRequests>>) {
  const nextMap = new Map<string, string[]>();

  requests.forEach((request) => {
    const parentRequestId = normalizeText(request.parentRequestId);
    const requestId = normalizeText(request.requestId);

    if (!parentRequestId || !requestId) {
      return;
    }

    const currentChildRequestIds = nextMap.get(parentRequestId) ?? [];
    currentChildRequestIds.push(requestId);
    nextMap.set(parentRequestId, currentChildRequestIds);
  });

  return nextMap;
}

function collectShareScopeRequestIds(rootRequestId: string, childRequestIdsByParentRequestId: Map<string, string[]>) {
  const normalizedRootRequestId = normalizeText(rootRequestId);

  if (!normalizedRootRequestId) {
    return [];
  }

  const orderedRequestIds: string[] = [];
  const pendingRequestIds = [normalizedRootRequestId];
  const visitedRequestIds = new Set<string>();

  while (pendingRequestIds.length > 0) {
    const currentRequestId = normalizeText(pendingRequestIds.shift());

    if (!currentRequestId || visitedRequestIds.has(currentRequestId)) {
      continue;
    }

    visitedRequestIds.add(currentRequestId);
    orderedRequestIds.push(currentRequestId);
    (childRequestIdsByParentRequestId.get(currentRequestId) ?? []).forEach((childRequestId) => {
      if (!visitedRequestIds.has(childRequestId)) {
        pendingRequestIds.push(childRequestId);
      }
    });
  }

  return orderedRequestIds;
}

async function resolveChatShareFallbackUsageSummary(
  token: SharedResourceTokenRecord,
): Promise<{
  tokenId: string;
  usageTokenTotal: number;
  usageRequestCount: number;
  usageCompletedRequestCount: number;
  lastTokenUsedAt: string | null;
} | null> {
  if (token.resourceType !== 'chat-share') {
    return null;
  }

  const tokenPayload = parseChatShareTokenFromPath(token.sharePath || token.resourcePath);

  if (!tokenPayload?.sessionId || !tokenPayload.requestId) {
    return null;
  }

  const requests = await listChatConversationRequests(tokenPayload.sessionId, 1000);
  const requestMap = new Map(requests.map((request) => [normalizeText(request.requestId), request] as const));
  const targetRequest = requestMap.get(tokenPayload.requestId);

  if (!targetRequest) {
    return null;
  }

  const childRequestIdsByParentRequestId = buildChildRequestIdsByParentRequestId(requests);
  const rootRequestId = resolveShareRootRequestId(tokenPayload.requestId, requestMap, tokenPayload.kind);
  const scopeRequestIds = collectShareScopeRequestIds(rootRequestId, childRequestIdsByParentRequestId);

  if (scopeRequestIds.length === 0) {
    return null;
  }

  const scopeRequestIdSet = new Set(scopeRequestIds);
  const scopedRequests = requests.filter((request) => scopeRequestIdSet.has(normalizeText(request.requestId)));

  if (scopedRequests.length === 0) {
    return null;
  }

  await assignSharedResourceTokenToRequests(tokenPayload.sessionId, scopeRequestIds, token.id);

  const lastUsedAtMs = scopedRequests.reduce((latest, request) => Math.max(latest, resolveRequestUsageAtMs(request)), 0);

  return {
    tokenId: token.id,
    usageTokenTotal: scopedRequests.reduce((sum, request) => sum + resolveRequestUsageTotalTokens(request), 0),
    usageRequestCount: scopedRequests.length,
    usageCompletedRequestCount: scopedRequests.filter((request) => request.status === 'completed').length,
    lastTokenUsedAt: lastUsedAtMs > 0 ? new Date(lastUsedAtMs).toISOString() : null,
  };
}

function resolveSharePath(shareToken: string, inputPath: string | null) {
  const normalized = normalizeText(inputPath);
  return normalized || `/share/resource/${shareToken}`;
}

function normalizeTokenRecord(
  record: Partial<SharedResourceTokenRecord> & { accessPinHash?: unknown; tokenSettingSnapshot?: unknown },
): SharedResourceTokenRecord | null {
  const name = normalizeText(record.name);
  const resourceLabel = normalizeText(record.resourceLabel);
  const resourcePath = normalizeText(record.resourcePath);

  if (!name || !resourceLabel || !resourcePath) {
    return null;
  }

  const shareToken = normalizeText(record.shareToken) || createShareToken();
  const updatedAt = normalizeDateTime(record.updatedAt) ?? new Date().toISOString();

  return {
    id: normalizeText(record.id) || `srt_${shareToken.slice(0, 12)}`,
    name,
    description: normalizeText(record.description),
    tokenSettingId: normalizeOptionalText(record.tokenSettingId),
    tokenSettingSnapshot: normalizeTokenSettingSnapshot((record as { tokenSettingSnapshot?: unknown }).tokenSettingSnapshot),
    resourceContext: normalizeResourceContext((record as { resourceContext?: unknown }).resourceContext),
    linkedTokenSetting: null,
    resourceLabel,
    resourcePath,
    resourceType: normalizeResourceType(record.resourceType),
    shareToken,
    sharePath: resolveSharePath(shareToken, record.sharePath ?? null),
    resourceAllowedAppIds: normalizeStringArray((record as { resourceAllowedAppIds?: unknown }).resourceAllowedAppIds),
    resourceAllowedAppIdsOverrideEnabled: true,
    allowedAppIds: normalizeStringArray((record as { allowedAppIds?: unknown }).allowedAppIds),
    permissions: normalizePermissions(record.permissions),
    enabled: normalizeBoolean(record.enabled, true),
    revokedAt: normalizeDateTime(record.revokedAt),
    deletedAt: normalizeDateTime((record as { deletedAt?: unknown }).deletedAt),
    expiresAt: normalizeDateTime(record.expiresAt),
    effectiveExpiresAt: normalizeDateTime(record.effectiveExpiresAt),
    usageLimit: normalizePositiveInteger(record.usageLimit, 0, 0, UNBOUNDED_NUMERIC_LIMIT),
    usageCount: normalizePositiveInteger(record.usageCount, 0, 0, UNBOUNDED_NUMERIC_LIMIT),
    hasAccessPin: normalizeText((record as { accessPinHash?: unknown }).accessPinHash).length > 0,
    accessPinPromptTtlMinutes: normalizeNullablePositiveInteger(
      (record as { accessPinPromptTtlMinutes?: unknown }).accessPinPromptTtlMinutes,
      0,
      7 * 24 * 60,
    ),
    allowAccessPinChangeWithoutManage: normalizeBoolean(
      (record as { allowAccessPinChangeWithoutManage?: unknown }).allowAccessPinChangeWithoutManage,
      false,
    ),
    usageTokenTotal: normalizePositiveInteger((record as { usageTokenTotal?: unknown }).usageTokenTotal, 0, 0, UNBOUNDED_NUMERIC_LIMIT),
    usageRequestCount: normalizePositiveInteger((record as { usageRequestCount?: unknown }).usageRequestCount, 0, 0, UNBOUNDED_NUMERIC_LIMIT),
    usageCompletedRequestCount: normalizePositiveInteger(
      (record as { usageCompletedRequestCount?: unknown }).usageCompletedRequestCount,
      0,
      0,
      UNBOUNDED_NUMERIC_LIMIT,
    ),
    lastUsedAt: normalizeDateTime(record.lastUsedAt),
    lastTokenUsedAt: normalizeDateTime((record as { lastTokenUsedAt?: unknown }).lastTokenUsedAt),
    createdAt: normalizeDateTime(record.createdAt) ?? updatedAt,
    updatedAt,
    lastActivityAt: normalizeDateTime(record.lastActivityAt),
    activityCount: normalizePositiveInteger(record.activityCount, 0, 0, UNBOUNDED_NUMERIC_LIMIT),
  };
}

export function resolveSharedResourceTokenEffectiveExpiresAt(
  token: Pick<SharedResourceTokenRecord, 'expiresAt' | 'createdAt'>,
  tokenSettingSnapshot?: Pick<SharedResourceTokenSettingSnapshot, 'defaultExpiresInMinutes'> | null,
) {
  const explicitExpiresAt = normalizeDateTime(token.expiresAt);

  if (explicitExpiresAt) {
    return explicitExpiresAt;
  }

  const defaultExpiresInMinutes = Number(tokenSettingSnapshot?.defaultExpiresInMinutes ?? 0);

  if (!Number.isFinite(defaultExpiresInMinutes) || defaultExpiresInMinutes <= 0) {
    return null;
  }

  const createdAtMs = Date.parse(normalizeText(token.createdAt));

  if (!Number.isFinite(createdAtMs)) {
    return null;
  }

  return new Date(createdAtMs + defaultExpiresInMinutes * 60 * 1000).toISOString();
}

function hasExplicitResourceAllowedAppIds(
  token: Pick<SharedResourceTokenRecord, 'resourceAllowedAppIds' | 'resourceAllowedAppIdsOverrideEnabled'>,
) {
  return token.resourceAllowedAppIdsOverrideEnabled || normalizeStringArray(token.resourceAllowedAppIds).length > 0;
}

function resolveTokenAllowedAppIds(
  token: Pick<SharedResourceTokenRecord, 'resourceAllowedAppIds' | 'allowedAppIds' | 'tokenSettingSnapshot'>,
) {
  const activeAllowedAppIds = normalizeStringArray(token.allowedAppIds);

  if (activeAllowedAppIds.length > 0) {
    return activeAllowedAppIds;
  }

  const resourceAllowedAppIds = normalizeStringArray(token.resourceAllowedAppIds);

  if (resourceAllowedAppIds.length > 0) {
    return resourceAllowedAppIds;
  }

  return normalizeStringArray(token.tokenSettingSnapshot?.allowedAppIds);
}

function resolveLinkedTokenSettingSummary(
  token: SharedResourceTokenRecord,
  settingsById: Map<string, TokenSettingRecord>,
): SharedResourceLinkedTokenSettingSummary | null {
  if (!token.tokenSettingId) {
    return null;
  }

  const matchedSetting = settingsById.get(token.tokenSettingId);

  if (!matchedSetting) {
    return {
      id: token.tokenSettingId,
      name: token.tokenSettingId,
      enabled: false,
      defaultExpiresInMinutes: 0,
      allowedAppIds: [],
      syncState: 'missing',
      syncMessage: '연결된 토큰 설정이 삭제되었거나 더 이상 존재하지 않습니다.',
    };
  }

  return {
    id: matchedSetting.id,
    name: matchedSetting.name,
    enabled: matchedSetting.enabled,
    defaultExpiresInMinutes: matchedSetting.defaultExpiresInMinutes,
    allowedAppIds: matchedSetting.allowedAppIds,
    syncState: 'ok',
    syncMessage: null,
  };
}

async function attachLinkedTokenSettings(tokens: SharedResourceTokenRecord[]) {
  if (tokens.length === 0) {
    return tokens;
  }

  const tokenSettings = await getTokenSettingsConfig();
  const settingsById = new Map(tokenSettings.map((item) => [item.id, item] as const));

  return tokens.map((token) => {
    const allowedAppIds = resolveTokenAllowedAppIds(token);

    return {
      ...token,
      allowedAppIds,
      linkedTokenSetting: resolveLinkedTokenSettingSummary(token, settingsById),
      effectiveExpiresAt: resolveSharedResourceTokenEffectiveExpiresAt(token, token.tokenSettingSnapshot),
    };
  });
}

async function attachRequestUsageSummaries(
  tokens: SharedResourceTokenRecord[],
  options?: {
    includeFallback?: boolean;
  },
) {
  if (tokens.length === 0) {
    return tokens;
  }

  const summaries = await listChatConversationRequestUsageBySharedResourceTokenIds(tokens.map((token) => token.id));
  const summaryByTokenId = new Map(summaries.map((summary) => [summary.sharedResourceTokenId, summary] as const));
  if (options?.includeFallback !== false) {
    const unresolvedTokens = tokens.filter((token) => !summaryByTokenId.has(token.id));
    const fallbackSummaries = await Promise.all(unresolvedTokens.map((token) => resolveChatShareFallbackUsageSummary(token)));

    fallbackSummaries.forEach((summary) => {
      if (!summary) {
        return;
      }

      summaryByTokenId.set(summary.tokenId, {
        sharedResourceTokenId: summary.tokenId,
        requestCount: summary.usageRequestCount,
        completedRequestCount: summary.usageCompletedRequestCount,
        totalTokens: summary.usageTokenTotal,
        lastUsedAt: summary.lastTokenUsedAt,
      });
    });
  }

  return tokens.map((token) => {
    const summary = summaryByTokenId.get(token.id);

    if (!summary) {
      return token;
    }

    return {
      ...token,
      usageTokenTotal: summary.totalTokens,
      usageRequestCount: summary.requestCount,
      usageCompletedRequestCount: summary.completedRequestCount,
      lastTokenUsedAt: summary.lastUsedAt,
    };
  });
}

function mapTokenRow(row: Record<string, unknown>): SharedResourceTokenRecord | null {
  let parsedPermissions: unknown[] = [];
  let parsedTokenSettingSnapshot: unknown = null;
  let parsedResourceContext: unknown = null;
  let parsedAllowedAppIds: string[] = [];

  if (typeof row.permissions_json === 'string') {
    try {
      const parsed = JSON.parse(row.permissions_json);
      parsedPermissions = Array.isArray(parsed) ? parsed : [];
    } catch {
      parsedPermissions = [];
    }
  }

  if (typeof row.token_setting_snapshot_json === 'string') {
    try {
      parsedTokenSettingSnapshot = JSON.parse(row.token_setting_snapshot_json);
    } catch {
      parsedTokenSettingSnapshot = null;
    }
  }

  if (typeof row.allowed_app_ids_json === 'string') {
    try {
      const parsed = JSON.parse(row.allowed_app_ids_json);
      parsedAllowedAppIds = normalizeStringArray(parsed);
    } catch {
      parsedAllowedAppIds = [];
    }
  }

  if (typeof row.resource_context_json === 'string') {
    try {
      parsedResourceContext = JSON.parse(row.resource_context_json);
    } catch {
      parsedResourceContext = null;
    }
  }

  return normalizeTokenRecord({
    id: normalizeText(row.id),
    name: normalizeText(row.name),
    description: normalizeText(row.description),
    tokenSettingId: normalizeOptionalText(row.token_setting_id),
    resourceLabel: normalizeText(row.resource_label),
    resourcePath: normalizeText(row.resource_path),
    resourceType: normalizeResourceType(row.resource_type),
    shareToken: normalizeText(row.share_token),
    sharePath: normalizeText(row.share_path),
    resourceAllowedAppIds: parsedAllowedAppIds,
    resourceAllowedAppIdsOverrideEnabled: normalizeBoolean(row.allowed_app_ids_override_enabled, false),
    allowedAppIds: normalizeStringArray(parsedAllowedAppIds),
    permissions: normalizePermissions(parsedPermissions),
    tokenSettingSnapshot: normalizeTokenSettingSnapshot(parsedTokenSettingSnapshot),
    resourceContext: normalizeResourceContext(parsedResourceContext),
    accessPinHash: normalizeText(row.access_pin_hash),
    allowAccessPinChangeWithoutManage: normalizeBoolean(row.allow_access_pin_change_without_manage, false),
    enabled: normalizeBoolean(row.enabled, true),
    revokedAt: normalizeDateTime(row.revoked_at),
    deletedAt: normalizeDateTime(row.deleted_at),
    expiresAt: normalizeDateTime(row.expires_at),
    usageLimit: normalizePositiveInteger(row.usage_limit, 0, 0, UNBOUNDED_NUMERIC_LIMIT),
    usageCount: normalizePositiveInteger(row.usage_count, 0, 0, UNBOUNDED_NUMERIC_LIMIT),
    accessPinPromptTtlMinutes: normalizeNullablePositiveInteger(row.access_pin_prompt_ttl_minutes, 0, 7 * 24 * 60),
    lastUsedAt: normalizeDateTime(row.last_used_at),
    createdAt: normalizeDateTime(row.created_at) ?? new Date().toISOString(),
    updatedAt: normalizeDateTime(row.updated_at) ?? new Date().toISOString(),
    lastActivityAt: normalizeDateTime(row.last_activity_at),
    activityCount: normalizePositiveInteger(row.activity_count, 0, 0, UNBOUNDED_NUMERIC_LIMIT),
  });
}

function mapActivityRow(row: Record<string, unknown>): SharedResourceTokenActivityRecord {
  return {
    id: Number(row.id),
    tokenId: normalizeText(row.token_id),
    type: sharedResourceActivityInputSchema.shape.type.parse(row.activity_type),
    actorLabel: normalizeOptionalText(row.actor_label),
    summary: normalizeText(row.summary),
    detail: normalizeOptionalText(row.detail),
    usageDelta: normalizePositiveInteger(row.usage_delta, 0, 0, 1_000_000),
    clientIp: normalizeOptionalText(row.client_ip),
    externalIp: normalizeOptionalText(row.external_ip),
    forwardedFor: normalizeOptionalText(row.forwarded_for),
    realIp: normalizeOptionalText(row.real_ip),
    host: normalizeOptionalText(row.host),
    origin: normalizeOptionalText(row.origin),
    referer: normalizeOptionalText(row.referer),
    userAgent: normalizeOptionalText(row.user_agent),
    clientId: normalizeOptionalText(row.client_id),
    createdAt: normalizeDateTime(row.created_at) ?? new Date().toISOString(),
  };
}

async function ensureSharedResourceTokenTables() {
  const hasTokensTable = await db.schema.hasTable(SHARED_RESOURCE_TOKENS_TABLE);

  if (!hasTokensTable) {
    await db.schema.createTable(SHARED_RESOURCE_TOKENS_TABLE, (table) => {
      table.string('id', 120).primary();
      table.string('name', 120).notNullable();
      table.text('description').notNullable().defaultTo('');
      table.string('token_setting_id', 120).nullable().index();
      table.text('token_setting_snapshot_json').nullable();
      table.text('resource_context_json').nullable();
      table.string('resource_label', 160).notNullable();
      table.text('resource_path').notNullable();
      table.string('resource_type', 40).notNullable().defaultTo('directory');
      table.string('share_token', 255).notNullable().unique();
      table.text('share_path').notNullable();
      table.text('permissions_json').notNullable().defaultTo('["view"]');
      table.text('allowed_app_ids_json').notNullable().defaultTo('[]');
      table.boolean('allowed_app_ids_override_enabled').notNullable().defaultTo(false);
      table.boolean('enabled').notNullable().defaultTo(true);
      table.timestamp('revoked_at', { useTz: true }).nullable();
      table.timestamp('deleted_at', { useTz: true }).nullable().index();
      table.timestamp('expires_at', { useTz: true }).nullable();
      table.bigInteger('usage_limit').notNullable().defaultTo(0);
      table.bigInteger('usage_count').notNullable().defaultTo(0);
      table.text('access_pin_hash').nullable();
      table.integer('access_pin_prompt_ttl_minutes').nullable();
      table.boolean('allow_access_pin_change_without_manage').notNullable().defaultTo(false);
      table.timestamp('last_used_at', { useTz: true }).nullable();
      table.timestamp('last_activity_at', { useTz: true }).nullable();
      table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
      table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
    });
  } else {
    const requiredColumns: Array<[string, (table: any) => void]> = [
      ['name', (table) => table.string('name', 120).notNullable().defaultTo('')],
      ['description', (table) => table.text('description').notNullable().defaultTo('')],
      ['token_setting_id', (table) => table.string('token_setting_id', 120).nullable().index()],
      ['token_setting_snapshot_json', (table) => table.text('token_setting_snapshot_json').nullable()],
      ['resource_context_json', (table) => table.text('resource_context_json').nullable()],
      ['resource_label', (table) => table.string('resource_label', 160).notNullable().defaultTo('')],
      ['resource_path', (table) => table.text('resource_path').notNullable().defaultTo('')],
      ['resource_type', (table) => table.string('resource_type', 40).notNullable().defaultTo('directory')],
      ['share_token', (table) => table.string('share_token', 255).notNullable().defaultTo('')],
      ['share_path', (table) => table.text('share_path').notNullable().defaultTo('')],
      ['permissions_json', (table) => table.text('permissions_json').notNullable().defaultTo('["view"]')],
      ['allowed_app_ids_json', (table) => table.text('allowed_app_ids_json').notNullable().defaultTo('[]')],
      ['allowed_app_ids_override_enabled', (table) => table.boolean('allowed_app_ids_override_enabled').notNullable().defaultTo(false)],
      ['enabled', (table) => table.boolean('enabled').notNullable().defaultTo(true)],
      ['revoked_at', (table) => table.timestamp('revoked_at', { useTz: true }).nullable()],
      ['deleted_at', (table) => table.timestamp('deleted_at', { useTz: true }).nullable().index()],
      ['expires_at', (table) => table.timestamp('expires_at', { useTz: true }).nullable()],
      ['usage_limit', (table) => table.bigInteger('usage_limit').notNullable().defaultTo(0)],
      ['usage_count', (table) => table.bigInteger('usage_count').notNullable().defaultTo(0)],
      ['access_pin_hash', (table) => table.text('access_pin_hash').nullable()],
      ['access_pin_prompt_ttl_minutes', (table) => table.integer('access_pin_prompt_ttl_minutes').nullable()],
      ['allow_access_pin_change_without_manage', (table) => table.boolean('allow_access_pin_change_without_manage').notNullable().defaultTo(false)],
      ['last_used_at', (table) => table.timestamp('last_used_at', { useTz: true }).nullable()],
      ['last_activity_at', (table) => table.timestamp('last_activity_at', { useTz: true }).nullable()],
      ['created_at', (table) => table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
      ['updated_at', (table) => table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
    ];

    for (const [columnName, createColumn] of requiredColumns) {
      const hasColumn = await db.schema.hasColumn(SHARED_RESOURCE_TOKENS_TABLE, columnName);

      if (!hasColumn) {
        await db.schema.alterTable(SHARED_RESOURCE_TOKENS_TABLE, (table) => {
          createColumn(table);
        });
      }
    }
  }

  const hasTokenSettingSnapshotColumn = await db.schema.hasColumn(SHARED_RESOURCE_TOKENS_TABLE, 'token_setting_snapshot_json');

  if (hasTokenSettingSnapshotColumn) {
    const legacyChatShareRows = await db(SHARED_RESOURCE_TOKENS_TABLE)
      .select(
        'id',
        'share_path',
        'resource_path',
        'token_setting_id',
        'token_setting_snapshot_json',
        'resource_context_json',
        'allowed_app_ids_json',
        'created_at',
        'expires_at',
      )
      .where({ resource_type: 'chat-share' })
      .where((query) => {
        query
          .whereNotNull('token_setting_id')
          .orWhereNull('token_setting_snapshot_json')
          .orWhereNull('resource_context_json');
      });

    for (const row of legacyChatShareRows) {
      if (!isLegacyChatShareTokenRowNeedingMigration(row)) {
        continue;
      }

      const sharePath = normalizeText(row.share_path) || normalizeText(row.resource_path);
      const tokenPayload = parseChatShareTokenFromPath(sharePath);
      const snapshot = buildChatShareTokenSettingSnapshotFromPayload(tokenPayload, {
        createdAt: row.created_at,
        expiresAt: row.expires_at,
        fallbackTokenSettingId: row.token_setting_id,
      });
      const resourceContext = tokenPayload
        ? normalizeResourceContext({
            kind: tokenPayload.kind,
            sessionId: tokenPayload.sessionId,
            requestId: tokenPayload.requestId,
            sourceMessageId: tokenPayload.sourceMessageId,
            promptIndex: tokenPayload.promptIndex,
            promptSignature: tokenPayload.promptSignature,
          })
        : null;
      let migratedAllowedAppIds = snapshot?.allowedAppIds ?? [];

      if (!migratedAllowedAppIds.length && typeof row.allowed_app_ids_json === 'string') {
        try {
          migratedAllowedAppIds = normalizeStringArray(JSON.parse(row.allowed_app_ids_json));
        } catch {
          migratedAllowedAppIds = [];
        }
      }

      await db(SHARED_RESOURCE_TOKENS_TABLE)
        .where({ id: normalizeText(row.id) })
        .update({
          token_setting_id: null,
          token_setting_snapshot_json: snapshot ? JSON.stringify(snapshot) : null,
          resource_context_json: resourceContext ? JSON.stringify(resourceContext) : null,
          allowed_app_ids_json: JSON.stringify(migratedAllowedAppIds),
          allowed_app_ids_override_enabled: true,
        });
    }
  }

  const hasActivitiesTable = await db.schema.hasTable(SHARED_RESOURCE_TOKEN_ACTIVITIES_TABLE);

  if (!hasActivitiesTable) {
    await db.schema.createTable(SHARED_RESOURCE_TOKEN_ACTIVITIES_TABLE, (table) => {
      table.increments('id').primary();
      table.string('token_id', 120).notNullable().index();
      table.string('activity_type', 40).notNullable();
      table.string('actor_label', 120).nullable();
      table.string('summary', 400).notNullable();
      table.text('detail').nullable();
      table.integer('usage_delta').notNullable().defaultTo(0);
      table.string('client_ip', 120).nullable();
      table.string('external_ip', 120).nullable();
      table.text('forwarded_for').nullable();
      table.string('real_ip', 120).nullable();
      table.string('host', 255).nullable();
      table.text('origin').nullable();
      table.text('referer').nullable();
      table.text('user_agent').nullable();
      table.string('client_id', 255).nullable();
      table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
    });
    return;
  }

  const requiredActivityColumns: Array<[string, (table: any) => void]> = [
    ['token_id', (table) => table.string('token_id', 120).notNullable().index()],
    ['activity_type', (table) => table.string('activity_type', 40).notNullable().defaultTo('updated')],
    ['actor_label', (table) => table.string('actor_label', 120).nullable()],
    ['summary', (table) => table.string('summary', 400).notNullable().defaultTo('')],
    ['detail', (table) => table.text('detail').nullable()],
    ['usage_delta', (table) => table.integer('usage_delta').notNullable().defaultTo(0)],
    ['client_ip', (table) => table.string('client_ip', 120).nullable()],
    ['external_ip', (table) => table.string('external_ip', 120).nullable()],
    ['forwarded_for', (table) => table.text('forwarded_for').nullable()],
    ['real_ip', (table) => table.string('real_ip', 120).nullable()],
    ['host', (table) => table.string('host', 255).nullable()],
    ['origin', (table) => table.text('origin').nullable()],
    ['referer', (table) => table.text('referer').nullable()],
    ['user_agent', (table) => table.text('user_agent').nullable()],
    ['client_id', (table) => table.string('client_id', 255).nullable()],
    ['created_at', (table) => table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
  ];

  for (const [columnName, createColumn] of requiredActivityColumns) {
    const hasColumn = await db.schema.hasColumn(SHARED_RESOURCE_TOKEN_ACTIVITIES_TABLE, columnName);

    if (!hasColumn) {
      await db.schema.alterTable(SHARED_RESOURCE_TOKEN_ACTIVITIES_TABLE, (table) => {
        createColumn(table);
      });
    }
  }

  const hasAccessPinSessionsTable = await db.schema.hasTable(SHARED_RESOURCE_ACCESS_PIN_SESSIONS_TABLE);

  if (!hasAccessPinSessionsTable) {
    await db.schema.createTable(SHARED_RESOURCE_ACCESS_PIN_SESSIONS_TABLE, (table) => {
      table.increments('id').primary();
      table.text('share_path').notNullable().index();
      table.string('client_id', 255).notNullable().index();
      table.text('access_pin_hash').notNullable();
      table.timestamp('authenticated_at', { useTz: true }).notNullable();
      table.timestamp('expires_at', { useTz: true }).notNullable().index();
      table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
      table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
    });
    return;
  }

  const requiredSessionColumns: Array<[string, (table: any) => void]> = [
    ['share_path', (table) => table.text('share_path').notNullable().defaultTo('')],
    ['client_id', (table) => table.string('client_id', 255).notNullable().defaultTo('')],
    ['access_pin_hash', (table) => table.text('access_pin_hash').notNullable().defaultTo('')],
    ['authenticated_at', (table) => table.timestamp('authenticated_at', { useTz: true }).nullable()],
    ['expires_at', (table) => table.timestamp('expires_at', { useTz: true }).nullable()],
    ['created_at', (table) => table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
    ['updated_at', (table) => table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
  ];

  for (const [columnName, createColumn] of requiredSessionColumns) {
    const hasColumn = await db.schema.hasColumn(SHARED_RESOURCE_ACCESS_PIN_SESSIONS_TABLE, columnName);

    if (!hasColumn) {
      await db.schema.alterTable(SHARED_RESOURCE_ACCESS_PIN_SESSIONS_TABLE, (table) => {
        createColumn(table);
      });
    }
  }
}

async function clearExpiredSharedResourceAccessPinSessions(nowIso: string) {
  await db(SHARED_RESOURCE_ACCESS_PIN_SESSIONS_TABLE)
    .where('expires_at', '<=', nowIso)
    .delete();
}

async function resolveSharedResourceAccessPinSession(
  sharePath: string,
  clientId: string,
  accessPinHash: string,
  nowIso: string,
) {
  await clearExpiredSharedResourceAccessPinSessions(nowIso);

  const row = await db(SHARED_RESOURCE_ACCESS_PIN_SESSIONS_TABLE)
    .select('expires_at')
    .where({
      share_path: sharePath,
      client_id: clientId,
      access_pin_hash: accessPinHash,
    })
    .orderBy('authenticated_at', 'desc')
    .first();

  const expiresAt = normalizeDateTime(row?.expires_at);

  if (!expiresAt) {
    return null;
  }

  return Date.parse(expiresAt) > Date.parse(nowIso) ? expiresAt : null;
}

function buildSharedResourceAccessPinSessionExpiresAt(nowIso: string, promptTtlMinutes: number) {
  const nowMs = Date.parse(nowIso);

  if (!Number.isFinite(nowMs) || promptTtlMinutes <= 0) {
    return null;
  }

  return new Date(nowMs + promptTtlMinutes * 60 * 1000).toISOString();
}

async function persistSharedResourceAccessPinSession(
  sharePath: string,
  clientId: string,
  accessPinHash: string,
  expiresAtIso: string,
) {
  const authenticatedAtIso = new Date().toISOString();

  await db.transaction(async (trx) => {
    await trx(SHARED_RESOURCE_ACCESS_PIN_SESSIONS_TABLE)
      .where({
        share_path: sharePath,
        client_id: clientId,
      })
      .delete();

    await trx(SHARED_RESOURCE_ACCESS_PIN_SESSIONS_TABLE).insert({
      share_path: sharePath,
      client_id: clientId,
      access_pin_hash: accessPinHash,
      authenticated_at: authenticatedAtIso,
      expires_at: expiresAtIso,
      created_at: authenticatedAtIso,
      updated_at: authenticatedAtIso,
    });
  });
}

async function appendActivity(
  trx: any,
  tokenId: string,
  input: SharedResourceActivityInput & { audit?: RequestAuditContext | null },
) {
  const payload = sharedResourceActivityInputSchema.parse(input);

  await trx(SHARED_RESOURCE_TOKEN_ACTIVITIES_TABLE).insert({
    token_id: tokenId,
    activity_type: payload.type,
    actor_label: payload.actorLabel ?? null,
    summary: payload.summary,
    detail: payload.detail ?? null,
    usage_delta: payload.usageDelta ?? 0,
    client_ip: input.audit?.clientIp ?? null,
    external_ip: input.audit?.externalIp ?? null,
    forwarded_for: input.audit?.forwardedFor ?? null,
    real_ip: input.audit?.realIp ?? null,
    host: input.audit?.host ?? null,
    origin: input.audit?.origin ?? null,
    referer: input.audit?.referer ?? null,
    user_agent: input.audit?.userAgent ?? null,
    client_id: input.audit?.clientId ?? null,
    created_at: db.fn.now(),
  });

  await trx(SHARED_RESOURCE_TOKENS_TABLE)
    .where({ id: tokenId })
    .update({
      last_activity_at: db.fn.now(),
      updated_at: db.fn.now(),
    });
}

async function listActivitiesByTokenId(tokenId: string) {
  await ensureSharedResourceTokenTables();

  const rows = await db(SHARED_RESOURCE_TOKEN_ACTIVITIES_TABLE)
    .select('*')
    .where({ token_id: tokenId })
    .orderBy('created_at', 'desc')
    .limit(200);

  return rows.map((row) => mapActivityRow(row as Record<string, unknown>));
}

export async function listSharedResourceTokens() {
  await ensureSharedResourceTokenTables();

  const rows = await db(SHARED_RESOURCE_TOKENS_TABLE)
    .leftJoin(
      db(SHARED_RESOURCE_TOKEN_ACTIVITIES_TABLE)
        .select('token_id')
        .count<{ token_id: string; activity_count: number }[]>({ activity_count: '*' })
        .max<{ token_id: string; last_activity_at: string | null }[]>({ last_activity_at: 'created_at' })
        .groupBy('token_id')
        .as('activity_meta'),
      'activity_meta.token_id',
      `${SHARED_RESOURCE_TOKENS_TABLE}.id`,
    )
    .select(
      `${SHARED_RESOURCE_TOKENS_TABLE}.*`,
      db.raw('COALESCE(activity_meta.activity_count, 0) as activity_count'),
      db.raw(`COALESCE(activity_meta.last_activity_at, ${SHARED_RESOURCE_TOKENS_TABLE}.updated_at) as last_activity_at`),
    )
    .whereNull(`${SHARED_RESOURCE_TOKENS_TABLE}.deleted_at`)
    .orderBy(`${SHARED_RESOURCE_TOKENS_TABLE}.updated_at`, 'desc');

  const tokens = rows
    .map((row) => mapTokenRow(row as Record<string, unknown>))
    .filter((item): item is SharedResourceTokenRecord => Boolean(item));

  return attachRequestUsageSummaries(await attachLinkedTokenSettings(tokens), { includeFallback: false });
}

async function getSharedResourceTokenDetailInternal(tokenId: string, options?: { includeDeleted?: boolean }) {
  await ensureSharedResourceTokenTables();

  const query = db(SHARED_RESOURCE_TOKENS_TABLE).where({ id: normalizeText(tokenId) });

  if (!options?.includeDeleted) {
    query.whereNull('deleted_at');
  }

  const row = await query.first();

  if (!row) {
    return null;
  }

  const token = mapTokenRow(row as Record<string, unknown>);

  if (!token) {
    return null;
  }

  const [linkedToken] = await attachRequestUsageSummaries(await attachLinkedTokenSettings([token]));

  return {
    token: linkedToken,
    activities: await listActivitiesByTokenId(token.id),
  };
}

export async function getSharedResourceTokenDetail(tokenId: string) {
  return getSharedResourceTokenDetailInternal(tokenId);
}

export async function getSharedResourceTokenDetailBySharePath(sharePath: string) {
  await ensureSharedResourceTokenTables();

  const normalizedSharePath = normalizeText(sharePath);

  if (!normalizedSharePath) {
    return null;
  }

  const row = await db(SHARED_RESOURCE_TOKENS_TABLE)
    .where({ share_path: normalizedSharePath })
    .whereNull('deleted_at')
    .first();

  if (!row) {
    return null;
  }

  const token = mapTokenRow(row as Record<string, unknown>);

  if (!token) {
    return null;
  }

  const [linkedToken] = await attachRequestUsageSummaries(await attachLinkedTokenSettings([token]));

  return {
    token: linkedToken,
    activities: await listActivitiesByTokenId(token.id),
  };
}

export async function getSharedResourceTokenDetailByShareToken(shareToken: string) {
  await ensureSharedResourceTokenTables();

  const normalizedShareToken = normalizeText(shareToken);

  if (!normalizedShareToken) {
    return null;
  }

  const row = await db(SHARED_RESOURCE_TOKENS_TABLE)
    .where({ share_token: normalizedShareToken })
    .whereNull('deleted_at')
    .first();

  if (!row) {
    return null;
  }

  const token = mapTokenRow(row as Record<string, unknown>);

  if (!token) {
    return null;
  }

  const [linkedToken] = await attachRequestUsageSummaries(await attachLinkedTokenSettings([token]));

  return {
    token: linkedToken,
    activities: await listActivitiesByTokenId(token.id),
  };
}

export async function validateSharedResourceAccessPinBySharePath(
  sharePath: string,
  providedPin?: string | null,
  options?: {
    clientId?: string | null;
  },
) {
  await ensureSharedResourceTokenTables();

  const normalizedSharePath = normalizeText(sharePath);

  if (!normalizedSharePath) {
    return { status: 'missing', hasAccessPin: false, sessionExpiresAt: null } as const;
  }

  const row = await db(SHARED_RESOURCE_TOKENS_TABLE)
    .select('share_token', 'access_pin_hash', 'access_pin_prompt_ttl_minutes')
    .where({ share_path: normalizedSharePath })
    .first();

  if (!row) {
    return { status: 'missing', hasAccessPin: false, sessionExpiresAt: null } as const;
  }

  const storedHash = normalizeText(row.access_pin_hash);
  const promptTtlMinutes = normalizeNullablePositiveInteger(row.access_pin_prompt_ttl_minutes, 0, 7 * 24 * 60) ?? 0;
  const normalizedClientId = normalizeText(options?.clientId);
  const nowIso = new Date().toISOString();

  if (!storedHash) {
    return { status: 'not-configured', hasAccessPin: false, sessionExpiresAt: null } as const;
  }

  if (normalizedClientId && promptTtlMinutes > 0) {
    const sessionExpiresAt = await resolveSharedResourceAccessPinSession(
      normalizedSharePath,
      normalizedClientId,
      storedHash,
      nowIso,
    );

    if (sessionExpiresAt) {
      const refreshedSessionExpiresAt = buildSharedResourceAccessPinSessionExpiresAt(nowIso, promptTtlMinutes);

      if (refreshedSessionExpiresAt) {
        await persistSharedResourceAccessPinSession(
          normalizedSharePath,
          normalizedClientId,
          storedHash,
          refreshedSessionExpiresAt,
        );
      }

      return {
        status: 'ok',
        hasAccessPin: true,
        sessionExpiresAt: refreshedSessionExpiresAt ?? sessionExpiresAt,
      } as const;
    }
  }

  const normalizedPin = normalizeText(providedPin);

  if (!normalizedPin) {
    return { status: 'required', hasAccessPin: true, sessionExpiresAt: null } as const;
  }

  if (!/^\d{4}$/u.test(normalizedPin)) {
    return { status: 'invalid', hasAccessPin: true, sessionExpiresAt: null } as const;
  }

  const isValid = hashAccessPin(normalizeText(row.share_token), normalizedPin) === storedHash;

  if (!isValid) {
    return {
      status: 'invalid',
      hasAccessPin: true,
      sessionExpiresAt: null,
    } as const;
  }

  let sessionExpiresAt: string | null = null;

  if (normalizedClientId && promptTtlMinutes > 0) {
    sessionExpiresAt = buildSharedResourceAccessPinSessionExpiresAt(nowIso, promptTtlMinutes);

    if (sessionExpiresAt) {
      await persistSharedResourceAccessPinSession(normalizedSharePath, normalizedClientId, storedHash, sessionExpiresAt);
    }
  }

  return {
    status: 'ok',
    hasAccessPin: true,
    sessionExpiresAt,
  } as const;
}

export async function upsertSharedResourceToken(
  input: SharedResourceTokenInput,
  options?: { actorLabel?: string | null; audit?: RequestAuditContext | null },
) {
  await ensureSharedResourceTokenTables();

  const parsed = sharedResourceTokenSchema.parse(input);
  const existingRow = parsed.id ? await db(SHARED_RESOURCE_TOKENS_TABLE).where({ id: normalizeText(parsed.id) }).first() : null;
  const existing = parsed.id ? await getSharedResourceTokenDetail(parsed.id) : null;
  const resolvedName = normalizeText(parsed.name ?? parsed.title ?? existing?.token.name);
  const resolvedResourceLabel = normalizeText(parsed.resourceLabel ?? resolvedName ?? existing?.token.resourceLabel);
  const requestedTokenSettingId = parsed.resourceType === 'chat-share' ? null : normalizeOptionalText(parsed.tokenSettingId);
  const effectiveTokenSettingId = requestedTokenSettingId ?? existing?.token.tokenSettingId ?? null;

  if (!resolvedName) {
    throw new Error('공유 리소스 이름을 입력해 주세요.');
  }

  if (!resolvedResourceLabel) {
    throw new Error('공유 리소스 표시 이름을 입력해 주세요.');
  }

  const shouldRefreshTokenSettingSnapshot =
    parsed.resourceType !== 'chat-share'
    && Boolean(effectiveTokenSettingId)
    && (
      normalizeTokenSettingSnapshot(parsed.tokenSettingSnapshot) == null
      || !existing?.token.tokenSettingSnapshot
      || existing.token.tokenSettingId !== effectiveTokenSettingId
    );
  const loadedTokenSetting =
    shouldRefreshTokenSettingSnapshot && effectiveTokenSettingId
      ? await getTokenSettingById(effectiveTokenSettingId)
      : null;

  if (shouldRefreshTokenSettingSnapshot && effectiveTokenSettingId && !loadedTokenSetting) {
    throw new Error('연결 토큰 설정을 찾을 수 없습니다.');
  }

  const tokenSettingSnapshot =
    normalizeTokenSettingSnapshot(parsed.tokenSettingSnapshot)
    ?? (loadedTokenSetting ? buildTokenSettingSnapshotFromRecord(loadedTokenSetting) : null)
    ?? existing?.token.tokenSettingSnapshot
    ?? null;
  const tokenSettingId = parsed.resourceType === 'chat-share' ? null : effectiveTokenSettingId;
  const previousAccessPinHash = normalizeText(existingRow?.access_pin_hash);
  const nextAllowedAppIds = normalizeStringArray(
    parsed.resourceAllowedAppIds
    ?? parsed.tokenSettingSnapshot?.allowedAppIds
    ?? existing?.token.allowedAppIds
    ?? tokenSettingSnapshot?.allowedAppIds
    ?? [],
  );
  const nextResourceAllowedAppIds = nextAllowedAppIds;
  const nextResourceAllowedAppIdsOverrideEnabled = true;
  const requestedShareToken = normalizeText(parsed.shareToken);
  const fallbackShareToken = normalizeText(existing?.token.shareToken);

  const nextResourceContext = normalizeResourceContext(parsed.resourceContext ?? existing?.token.resourceContext ?? null);

  let savedRecord: SharedResourceTokenRecord | null = null;

  for (let attempt = 0; attempt < 5; attempt += 1) {
    const effectiveShareToken =
      (attempt === 0 ? requestedShareToken || fallbackShareToken : '') || createShareToken();
    const shareTokenChanged = effectiveShareToken !== fallbackShareToken;
    const nextAccessPinHash =
      typeof parsed.accessPin === 'string'
        ? hashAccessPin(effectiveShareToken, parsed.accessPin)
        : parsed.accessPin === null
          ? null
          : shareTokenChanged
            ? null
            : previousAccessPinHash || null;
    const nextAccessPinPromptTtlMinutes =
      parsed.accessPin === null || (shareTokenChanged && previousAccessPinHash)
        ? null
        : parsed.accessPinPromptTtlMinutes ?? existing?.token.accessPinPromptTtlMinutes ?? null;
    const nextRecord = normalizeTokenRecord({
      ...existing?.token,
      id: parsed.id ?? existing?.token.id,
      name: resolvedName,
      description: parsed.description ?? existing?.token.description ?? '',
      tokenSettingId,
      tokenSettingSnapshot,
      resourceContext: nextResourceContext,
      resourceLabel: resolvedResourceLabel,
      resourcePath: parsed.resourcePath,
      resourceType: parsed.resourceType,
      shareToken: effectiveShareToken,
      sharePath: parsed.sharePath ?? existing?.token.sharePath ?? '',
      resourceAllowedAppIds: nextResourceAllowedAppIds,
      resourceAllowedAppIdsOverrideEnabled: nextResourceAllowedAppIdsOverrideEnabled,
      allowedAppIds: nextAllowedAppIds,
      permissions: parsed.permissions,
      accessPinHash: nextAccessPinHash,
      accessPinPromptTtlMinutes: nextAccessPinPromptTtlMinutes,
      allowAccessPinChangeWithoutManage:
        parsed.allowAccessPinChangeWithoutManage ?? existing?.token.allowAccessPinChangeWithoutManage ?? false,
      enabled: parsed.enabled,
      expiresAt: parsed.expiresAt ?? existing?.token.expiresAt ?? null,
      usageLimit: parsed.usageLimit,
      usageCount: existing?.token.usageCount ?? 0,
      lastUsedAt: existing?.token.lastUsedAt ?? null,
      createdAt: existing?.token.createdAt ?? new Date().toISOString(),
      revokedAt: existing?.token.revokedAt ?? null,
      deletedAt: existing?.token.deletedAt ?? null,
      updatedAt: new Date().toISOString(),
    });

    if (!nextRecord) {
      throw new Error('공유 리소스 토큰 저장 값이 올바르지 않습니다.');
    }

    try {
      await db.transaction(async (trx) => {
        await trx(SHARED_RESOURCE_TOKENS_TABLE)
          .insert({
            id: nextRecord.id,
            name: nextRecord.name,
            description: nextRecord.description,
            token_setting_id: nextRecord.tokenSettingId,
            token_setting_snapshot_json: nextRecord.tokenSettingSnapshot ? JSON.stringify(nextRecord.tokenSettingSnapshot) : null,
            resource_context_json: nextRecord.resourceContext ? JSON.stringify(nextRecord.resourceContext) : null,
            resource_label: nextRecord.resourceLabel,
            resource_path: nextRecord.resourcePath,
            resource_type: nextRecord.resourceType,
            share_token: nextRecord.shareToken,
            share_path: nextRecord.sharePath,
            allowed_app_ids_json: JSON.stringify(nextRecord.resourceAllowedAppIds),
            allowed_app_ids_override_enabled: nextRecord.resourceAllowedAppIdsOverrideEnabled,
            permissions_json: JSON.stringify(nextRecord.permissions),
            enabled: nextRecord.enabled,
            revoked_at: nextRecord.revokedAt,
            deleted_at: nextRecord.deletedAt,
            expires_at: nextRecord.expiresAt,
            usage_limit: nextRecord.usageLimit,
            usage_count: nextRecord.usageCount,
            access_pin_hash: nextAccessPinHash,
            access_pin_prompt_ttl_minutes: nextRecord.accessPinPromptTtlMinutes,
            allow_access_pin_change_without_manage: nextRecord.allowAccessPinChangeWithoutManage,
            last_used_at: nextRecord.lastUsedAt,
            last_activity_at: nextRecord.lastActivityAt,
            created_at: nextRecord.createdAt,
            updated_at: nextRecord.updatedAt,
          })
          .onConflict('id')
          .merge({
            name: nextRecord.name,
            description: nextRecord.description,
            token_setting_id: nextRecord.tokenSettingId,
            token_setting_snapshot_json: nextRecord.tokenSettingSnapshot ? JSON.stringify(nextRecord.tokenSettingSnapshot) : null,
            resource_context_json: nextRecord.resourceContext ? JSON.stringify(nextRecord.resourceContext) : null,
            resource_label: nextRecord.resourceLabel,
            resource_path: nextRecord.resourcePath,
            resource_type: nextRecord.resourceType,
            share_token: nextRecord.shareToken,
            share_path: nextRecord.sharePath,
            allowed_app_ids_json: JSON.stringify(nextRecord.resourceAllowedAppIds),
            allowed_app_ids_override_enabled: nextRecord.resourceAllowedAppIdsOverrideEnabled,
            permissions_json: JSON.stringify(nextRecord.permissions),
            enabled: nextRecord.enabled,
            revoked_at: nextRecord.revokedAt,
            deleted_at: nextRecord.deletedAt,
            expires_at: nextRecord.expiresAt,
            usage_limit: nextRecord.usageLimit,
            access_pin_hash: nextAccessPinHash,
            access_pin_prompt_ttl_minutes: nextRecord.accessPinPromptTtlMinutes,
            allow_access_pin_change_without_manage: nextRecord.allowAccessPinChangeWithoutManage,
            last_activity_at: nextRecord.lastActivityAt,
            updated_at: nextRecord.updatedAt,
          });

        await appendActivity(trx, nextRecord.id, {
          type: existing ? 'updated' : 'created',
          actorLabel: options?.actorLabel ?? 'manager',
          summary: existing ? '공유 리소스 토큰을 수정했습니다.' : '공유 리소스 토큰을 생성했습니다.',
          detail: nextRecord.resourceLabel,
          audit: options?.audit,
        });

        const previousPermissions = new Set(existing?.token.permissions ?? []);
        const nextPermissions = new Set(nextRecord.permissions);

        for (const permission of nextRecord.permissions.filter((item) => !previousPermissions.has(item))) {
          await appendActivity(trx, nextRecord.id, {
            type: 'permission-granted',
            actorLabel: options?.actorLabel ?? 'manager',
            summary: `${permission} 권한을 추가했습니다.`,
            detail: nextRecord.resourceLabel,
            audit: options?.audit,
          });
        }

        for (const permission of (existing?.token.permissions ?? []).filter((item) => !nextPermissions.has(item))) {
          await appendActivity(trx, nextRecord.id, {
            type: 'permission-revoked',
            actorLabel: options?.actorLabel ?? 'manager',
            summary: `${permission} 권한을 회수했습니다.`,
            detail: nextRecord.resourceLabel,
            audit: options?.audit,
          });
        }

        const previousAllowedApps = new Set(existing?.token.allowedAppIds ?? []);
        const nextAllowedApps = new Set(nextRecord.allowedAppIds);

        for (const appId of nextRecord.allowedAppIds.filter((item) => !previousAllowedApps.has(item))) {
          await appendActivity(trx, nextRecord.id, {
            type: 'updated',
            actorLabel: options?.actorLabel ?? 'manager',
            summary: `${appId} 앱 권한을 추가했습니다.`,
            detail: nextRecord.resourceLabel,
            audit: options?.audit,
          });
        }

        for (const appId of (existing?.token.allowedAppIds ?? []).filter((item) => !nextAllowedApps.has(item))) {
          await appendActivity(trx, nextRecord.id, {
            type: 'updated',
            actorLabel: options?.actorLabel ?? 'manager',
            summary: `${appId} 앱 권한을 제거했습니다.`,
            detail: nextRecord.resourceLabel,
            audit: options?.audit,
          });
        }

        const previousHasAccessPin = existing?.token.hasAccessPin ?? false;
        const nextHasAccessPin = Boolean(nextAccessPinHash);
        const previousAccessPinPromptTtlMinutes = existing?.token.accessPinPromptTtlMinutes ?? null;
        const previousAllowAccessPinChangeWithoutManage = existing?.token.allowAccessPinChangeWithoutManage ?? false;

        if (!previousHasAccessPin && nextHasAccessPin) {
          await appendActivity(trx, nextRecord.id, {
            type: 'updated',
            actorLabel: options?.actorLabel ?? 'manager',
            summary: '공유 접근 비밀번호를 설정했습니다.',
            detail: nextRecord.resourceLabel,
            audit: options?.audit,
          });
        }

        if (previousHasAccessPin && !nextHasAccessPin) {
          await appendActivity(trx, nextRecord.id, {
            type: 'updated',
            actorLabel: options?.actorLabel ?? 'manager',
            summary: '공유 접근 비밀번호를 해제했습니다.',
            detail: nextRecord.resourceLabel,
            audit: options?.audit,
          });
        }

        if (previousHasAccessPin && nextHasAccessPin && typeof parsed.accessPin === 'string') {
          await appendActivity(trx, nextRecord.id, {
            type: 'updated',
            actorLabel: options?.actorLabel ?? 'manager',
            summary: '공유 접근 비밀번호를 변경했습니다.',
            detail: nextRecord.resourceLabel,
            audit: options?.audit,
          });
        }

        if (previousAccessPinPromptTtlMinutes !== nextRecord.accessPinPromptTtlMinutes) {
          await appendActivity(trx, nextRecord.id, {
            type: 'updated',
            actorLabel: options?.actorLabel ?? 'manager',
            summary: nextRecord.accessPinPromptTtlMinutes
              ? `공유 비밀번호 재입력 유지시간을 ${nextRecord.accessPinPromptTtlMinutes}분으로 변경했습니다.`
              : '공유 비밀번호 재입력 방식을 매번 묻기로 변경했습니다.',
            detail: nextRecord.resourceLabel,
            audit: options?.audit,
          });
        }

        if (!previousAllowAccessPinChangeWithoutManage && nextRecord.allowAccessPinChangeWithoutManage) {
          await appendActivity(trx, nextRecord.id, {
            type: 'updated',
            actorLabel: options?.actorLabel ?? 'manager',
            summary: '관리 권한 없이 비밀번호 변경 가능한 모드를 켰습니다.',
            detail: nextRecord.resourceLabel,
            audit: options?.audit,
          });
        }

        if (previousAllowAccessPinChangeWithoutManage && !nextRecord.allowAccessPinChangeWithoutManage) {
          await appendActivity(trx, nextRecord.id, {
            type: 'updated',
            actorLabel: options?.actorLabel ?? 'manager',
            summary: '관리 권한 없이 비밀번호 변경 가능한 모드를 껐습니다.',
            detail: nextRecord.resourceLabel,
            audit: options?.audit,
          });
        }
      });

      savedRecord = nextRecord;
      break;
    } catch (error) {
      const canRetryAutoGeneratedShareToken = !requestedShareToken && isSharedResourceShareTokenUniqueError(error);
      if (canRetryAutoGeneratedShareToken && attempt < 4) {
        continue;
      }

      if (isSharedResourceShareTokenUniqueError(error)) {
        throw new Error('이미 사용 중인 공유 토큰 값입니다. 다른 값을 입력하거나 비워서 자동 생성해 주세요.');
      }

      throw error;
    }
  }

  if (!savedRecord) {
    throw new Error('공유 리소스 토큰 저장에 실패했습니다.');
  }

  return getSharedResourceTokenDetail(savedRecord.id);
}

export async function revokeSharedResourceToken(
  tokenId: string,
  reason?: string | null,
  options?: { actorLabel?: string | null; audit?: RequestAuditContext | null },
) {
  await ensureSharedResourceTokenTables();

  const normalizedTokenId = normalizeText(tokenId);
  const existing = await getSharedResourceTokenDetail(normalizedTokenId);

  if (!existing) {
    return null;
  }

  await db.transaction(async (trx) => {
    await trx(SHARED_RESOURCE_TOKENS_TABLE)
      .where({ id: normalizedTokenId })
      .update({
        revoked_at: db.fn.now(),
        enabled: false,
        updated_at: db.fn.now(),
      });

    await appendActivity(trx, normalizedTokenId, {
      type: 'revoked',
      actorLabel: options?.actorLabel ?? 'manager',
      summary: '공유 토큰을 회수했습니다.',
      detail: normalizeText(reason) || existing.token.resourceLabel,
      audit: options?.audit,
    });
  });

  return getSharedResourceTokenDetail(normalizedTokenId);
}

export async function revokeSharedResourceTokens(
  tokenIds: string[],
  reason?: string | null,
  options?: { actorLabel?: string | null; audit?: RequestAuditContext | null },
): Promise<SharedResourceTokenBulkActionResult> {
  await ensureSharedResourceTokenTables();

  const requestedTokenIds = Array.from(new Set(tokenIds.map((tokenId) => normalizeText(tokenId)).filter(Boolean)));

  if (requestedTokenIds.length === 0) {
    return {
      requestedTokenIds: [],
      processedTokenIds: [],
      skippedTokenIds: [],
      missingTokenIds: [],
    };
  }

  const existingRows = await db(SHARED_RESOURCE_TOKENS_TABLE).select('id', 'resource_label', 'revoked_at').whereIn('id', requestedTokenIds);
  const existingById = new Map(
    existingRows.map((row) => [
      normalizeText(row.id),
      {
        resourceLabel: normalizeText(row.resource_label),
        revokedAt: normalizeDateTime(row.revoked_at),
      },
    ]),
  );
  const processedTokenIds: string[] = [];
  const skippedTokenIds: string[] = [];
  const missingTokenIds = requestedTokenIds.filter((tokenId) => !existingById.has(tokenId));

  await db.transaction(async (trx) => {
    for (const tokenId of requestedTokenIds) {
      const existing = existingById.get(tokenId);

      if (!existing) {
        continue;
      }

      if (existing.revokedAt) {
        skippedTokenIds.push(tokenId);
        continue;
      }

      await trx(SHARED_RESOURCE_TOKENS_TABLE)
        .where({ id: tokenId })
        .update({
          revoked_at: db.fn.now(),
          enabled: false,
          updated_at: db.fn.now(),
        });

      await appendActivity(trx, tokenId, {
        type: 'revoked',
        actorLabel: options?.actorLabel ?? 'manager',
        summary: '공유 토큰을 일괄 회수했습니다.',
        detail: normalizeText(reason) || existing.resourceLabel,
        audit: options?.audit,
      });

      processedTokenIds.push(tokenId);
    }
  });

  return {
    requestedTokenIds,
    processedTokenIds,
    skippedTokenIds,
    missingTokenIds,
  };
}

export async function restoreSharedResourceToken(
  tokenId: string,
  options?: { actorLabel?: string | null; audit?: RequestAuditContext | null },
) {
  await ensureSharedResourceTokenTables();

  const normalizedTokenId = normalizeText(tokenId);
  const existing = await getSharedResourceTokenDetail(normalizedTokenId);

  if (!existing) {
    return null;
  }

  await db.transaction(async (trx) => {
    await trx(SHARED_RESOURCE_TOKENS_TABLE)
      .where({ id: normalizedTokenId })
      .update({
        revoked_at: null,
        enabled: true,
        updated_at: db.fn.now(),
      });

    await appendActivity(trx, normalizedTokenId, {
      type: 'restored',
      actorLabel: options?.actorLabel ?? 'manager',
      summary: '공유 토큰을 다시 활성화했습니다.',
      detail: existing.token.resourceLabel,
      audit: options?.audit,
    });
  });

  return getSharedResourceTokenDetail(normalizedTokenId);
}

export async function deleteSharedResourceToken(
  tokenId: string,
  options?: { actorLabel?: string | null; audit?: RequestAuditContext | null },
) {
  await ensureSharedResourceTokenTables();

  const normalizedTokenId = normalizeText(tokenId);

  if (!normalizedTokenId) {
    return false;
  }

  const existing = await getSharedResourceTokenDetailInternal(normalizedTokenId, { includeDeleted: true });

  if (!existing || existing.token.deletedAt) {
    return false;
  }

  await db.transaction(async (trx) => {
    await trx(SHARED_RESOURCE_TOKENS_TABLE)
      .where({ id: normalizedTokenId })
      .update({
        enabled: false,
        revoked_at: existing.token.revokedAt ?? db.fn.now(),
        deleted_at: db.fn.now(),
        updated_at: db.fn.now(),
      });

    await appendActivity(trx, normalizedTokenId, {
      type: 'deleted',
      actorLabel: options?.actorLabel ?? 'manager',
      summary: '공유 토큰을 삭제 목록에서 숨기고 사용 이력을 보존했습니다.',
      detail: existing.token.resourceLabel,
      audit: options?.audit,
    });
  });

  return true;
}

export async function deleteSharedResourceTokens(
  tokenIds: string[],
  options?: { actorLabel?: string | null; audit?: RequestAuditContext | null },
): Promise<SharedResourceTokenBulkActionResult> {
  await ensureSharedResourceTokenTables();

  const requestedTokenIds = Array.from(new Set(tokenIds.map((tokenId) => normalizeText(tokenId)).filter(Boolean)));

  if (requestedTokenIds.length === 0) {
    return {
      requestedTokenIds: [],
      processedTokenIds: [],
      skippedTokenIds: [],
      missingTokenIds: [],
    };
  }

  const existingRows = await db(SHARED_RESOURCE_TOKENS_TABLE).select('id', 'deleted_at').whereIn('id', requestedTokenIds);
  const existingIdSet = new Set(existingRows.map((row) => normalizeText(row.id)).filter(Boolean));
  const deletedIdSet = new Set(
    existingRows
      .filter((row) => normalizeDateTime((row as { deleted_at?: unknown }).deleted_at))
      .map((row) => normalizeText(row.id))
      .filter(Boolean),
  );
  const processedTokenIds: string[] = [];
  const skippedTokenIds: string[] = requestedTokenIds.filter((tokenId) => deletedIdSet.has(tokenId));
  const missingTokenIds = requestedTokenIds.filter((tokenId) => !existingIdSet.has(tokenId));

  await db.transaction(async (trx) => {
    for (const tokenId of requestedTokenIds) {
      if (!existingIdSet.has(tokenId) || deletedIdSet.has(tokenId)) {
        continue;
      }

      const existing = await getSharedResourceTokenDetailInternal(tokenId, { includeDeleted: true });

      await trx(SHARED_RESOURCE_TOKENS_TABLE)
        .where({ id: tokenId })
        .update({
          enabled: false,
          revoked_at: existing?.token.revokedAt ?? db.fn.now(),
          deleted_at: db.fn.now(),
          updated_at: db.fn.now(),
        });

      await appendActivity(trx, tokenId, {
        type: 'deleted',
        actorLabel: options?.actorLabel ?? 'manager',
        summary: '공유 토큰을 삭제 목록에서 숨기고 사용 이력을 보존했습니다.',
        detail: existing?.token.resourceLabel ?? null,
        audit: options?.audit,
      });
      processedTokenIds.push(tokenId);
    }
  });

  return {
    requestedTokenIds,
    processedTokenIds,
    skippedTokenIds,
    missingTokenIds,
  };
}

export async function recordSharedResourceTokenUsage(
  tokenId: string,
  payload: {
    actorLabel?: string | null;
    summary?: string | null;
    detail?: string | null;
    usageDelta?: number | null;
    audit?: RequestAuditContext | null;
  } = {},
) {
  await ensureSharedResourceTokenTables();

  const normalizedTokenId = normalizeText(tokenId);
  const existing = await getSharedResourceTokenDetail(normalizedTokenId);

  if (!existing) {
    return null;
  }

  const usageDelta = normalizePositiveInteger(payload.usageDelta, 1, 1, 1_000_000);

  await db.transaction(async (trx) => {
    await trx(SHARED_RESOURCE_TOKENS_TABLE)
      .where({ id: normalizedTokenId })
      .update({
        usage_count: db.raw('usage_count + ?', [usageDelta]),
        last_used_at: db.fn.now(),
        updated_at: db.fn.now(),
      });

    await appendActivity(trx, normalizedTokenId, {
      type: 'usage',
      actorLabel: normalizeOptionalText(payload.actorLabel),
      summary: normalizeText(payload.summary) || '공유 URL 사용 이력을 기록했습니다.',
      detail: normalizeOptionalText(payload.detail),
      usageDelta,
      audit: payload.audit,
    });
  });

  return getSharedResourceTokenDetail(normalizedTokenId);
}
