import { db } from '../db/client.js';
import { appendTokenSettingActivity, ensureTokenSettingActivityTable } from './token-setting-activity-service.js';
import type { RequestAuditContext } from '../utils/request-audit.js';

const TOKEN_SETTINGS_TABLE = 'token_settings';
const UNBOUNDED_NUMERIC_LIMIT = Number.MAX_SAFE_INTEGER;

export type TokenSettingRecord = {
  id: string;
  name: string;
  description: string;
  defaultExpiresInMinutes: number;
  maxExpiresInMinutes: number;
  maxTokensPer30Days: number;
  maxTokensPer7Days: number;
  maxTokensPer5Hours: number;
  oneTimeTokenLimit: number;
  allowedAppIds: string[];
  enabled: boolean;
  updatedAt: string;
};

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

function normalizeEnabled(value: unknown) {
  if (typeof value === 'boolean') {
    return value;
  }

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

  if (typeof value === 'string') {
    const normalizedValue = value.trim().toLowerCase();

    if (['false', '0', 'n', 'no', 'off'].includes(normalizedValue)) {
      return false;
    }

    if (['true', '1', 'y', 'yes', 'on'].includes(normalizedValue)) {
      return true;
    }
  }

  return value !== false;
}

function normalizeSettingId(value: unknown) {
  return normalizeText(value)
    .toLowerCase()
    .replace(/\s+/g, '-')
    .replace(/[^a-z0-9._-]/g, '');
}

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 normalizeAllowedAppIds(value: unknown) {
  if (!Array.isArray(value)) {
    return [];
  }

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

function normalizeTokenSetting(record: Partial<TokenSettingRecord>): TokenSettingRecord | null {
  const id = normalizeSettingId(record.id);
  const name = normalizeText(record.name);

  if (!id || !name) {
    return null;
  }

  const defaultExpiresInMinutes = normalizePositiveInteger(record.defaultExpiresInMinutes, 60, 0, UNBOUNDED_NUMERIC_LIMIT);
  const resolvedMaxExpiresInMinutes = normalizePositiveInteger(
    record.maxExpiresInMinutes,
    defaultExpiresInMinutes <= 0 ? 0 : 10_080,
    0,
    UNBOUNDED_NUMERIC_LIMIT,
  );
  const maxExpiresInMinutes =
    defaultExpiresInMinutes <= 0 || resolvedMaxExpiresInMinutes <= 0
      ? 0
      : Math.max(defaultExpiresInMinutes, resolvedMaxExpiresInMinutes);

  const legacyMaxTotalTokens =
    'maxTotalTokens' in record
      ? normalizePositiveInteger((record as { maxTotalTokens?: number }).maxTotalTokens, 100_000, 0, UNBOUNDED_NUMERIC_LIMIT)
      : 100_000;

  return {
    id,
    name,
    description: normalizeText(record.description),
    defaultExpiresInMinutes,
    maxExpiresInMinutes,
    maxTokensPer30Days: normalizePositiveInteger(record.maxTokensPer30Days, legacyMaxTotalTokens, 0, UNBOUNDED_NUMERIC_LIMIT),
    maxTokensPer7Days: normalizePositiveInteger(record.maxTokensPer7Days, legacyMaxTotalTokens, 0, UNBOUNDED_NUMERIC_LIMIT),
    maxTokensPer5Hours: normalizePositiveInteger(record.maxTokensPer5Hours, legacyMaxTotalTokens, 0, UNBOUNDED_NUMERIC_LIMIT),
    oneTimeTokenLimit: normalizePositiveInteger(record.oneTimeTokenLimit, 0, 0, UNBOUNDED_NUMERIC_LIMIT),
    allowedAppIds: normalizeAllowedAppIds(record.allowedAppIds),
    enabled: normalizeEnabled(record.enabled),
    updatedAt: normalizeText(record.updatedAt) || new Date().toISOString(),
  };
}

function compareUpdatedAt(left: TokenSettingRecord, right: TokenSettingRecord) {
  const leftTime = Date.parse(left.updatedAt);
  const rightTime = Date.parse(right.updatedAt);

  if (Number.isFinite(leftTime) && Number.isFinite(rightTime) && leftTime !== rightTime) {
    return leftTime - rightTime;
  }

  return 0;
}

export function sanitizeTokenSettings(items: Partial<TokenSettingRecord>[] | null | undefined) {
  const byId = new Map<string, TokenSettingRecord>();

  for (const item of items ?? []) {
    const normalized = normalizeTokenSetting(item);
    if (!normalized) {
      continue;
    }

    const current = byId.get(normalized.id);
    if (!current || compareUpdatedAt(current, normalized) <= 0) {
      byId.set(normalized.id, normalized);
    }
  }

  return Array.from(byId.values()).sort((left, right) => {
    const nameCompare = left.name.localeCompare(right.name, 'ko-KR');
    if (nameCompare !== 0) {
      return nameCompare;
    }

    return left.id.localeCompare(right.id, 'en');
  });
}

async function ensureTokenSettingsTable() {
  const hasTable = await db.schema.hasTable(TOKEN_SETTINGS_TABLE);

  if (!hasTable) {
    await db.schema.createTable(TOKEN_SETTINGS_TABLE, (table) => {
      table.string('id').primary();
      table.string('name').notNullable();
      table.text('description').notNullable().defaultTo('');
      table.integer('default_expires_in_minutes').notNullable().defaultTo(60);
      table.integer('max_expires_in_minutes').notNullable().defaultTo(10_080);
      table.bigInteger('max_total_tokens').notNullable().defaultTo(100_000);
      table.bigInteger('max_tokens_per_30_days').notNullable().defaultTo(100_000);
      table.bigInteger('max_tokens_per_7_days').notNullable().defaultTo(100_000);
      table.bigInteger('max_tokens_per_5_hours').notNullable().defaultTo(100_000);
      table.bigInteger('one_time_token_limit').notNullable().defaultTo(0);
      table.text('allowed_app_ids_json').notNullable().defaultTo('[]');
      table.boolean('enabled').notNullable().defaultTo(true);
      table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
    });
    await ensureTokenSettingActivityTable();
    return;
  }

  const requiredColumns: Array<[string, (table: any) => void]> = [
    ['name', (table) => table.string('name').notNullable().defaultTo('')],
    ['description', (table) => table.text('description').notNullable().defaultTo('')],
    ['default_expires_in_minutes', (table) => table.integer('default_expires_in_minutes').notNullable().defaultTo(60)],
    ['max_expires_in_minutes', (table) => table.integer('max_expires_in_minutes').notNullable().defaultTo(10_080)],
    ['max_total_tokens', (table) => table.bigInteger('max_total_tokens').notNullable().defaultTo(100_000)],
    ['max_tokens_per_30_days', (table) => table.bigInteger('max_tokens_per_30_days').notNullable().defaultTo(100_000)],
    ['max_tokens_per_7_days', (table) => table.bigInteger('max_tokens_per_7_days').notNullable().defaultTo(100_000)],
    ['max_tokens_per_5_hours', (table) => table.bigInteger('max_tokens_per_5_hours').notNullable().defaultTo(100_000)],
    ['one_time_token_limit', (table) => table.bigInteger('one_time_token_limit').notNullable().defaultTo(0)],
    ['allowed_app_ids_json', (table) => table.text('allowed_app_ids_json').notNullable().defaultTo('[]')],
    ['enabled', (table) => table.boolean('enabled').notNullable().defaultTo(true)],
    ['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(TOKEN_SETTINGS_TABLE, columnName);

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

  await ensureTokenSettingActivityTable();
}

function parseAllowedAppIds(row: Record<string, unknown>) {
  const rawValue = row.allowed_app_ids_json;

  if (typeof rawValue !== 'string') {
    return [];
  }

  try {
    const parsed = JSON.parse(rawValue);
    return Array.isArray(parsed) ? parsed : [];
  } catch {
    return [];
  }
}

function toTokenSettingRecord(row: Record<string, unknown>) {
  return normalizeTokenSetting({
    id: typeof row.id === 'string' ? row.id : undefined,
    name: typeof row.name === 'string' ? row.name : undefined,
    description: typeof row.description === 'string' ? row.description : undefined,
    defaultExpiresInMinutes:
      typeof row.default_expires_in_minutes === 'number' || typeof row.default_expires_in_minutes === 'string'
        ? Number(row.default_expires_in_minutes)
        : undefined,
    maxExpiresInMinutes:
      typeof row.max_expires_in_minutes === 'number' || typeof row.max_expires_in_minutes === 'string'
        ? Number(row.max_expires_in_minutes)
        : undefined,
    maxTokensPer30Days:
      typeof row.max_tokens_per_30_days === 'number' || typeof row.max_tokens_per_30_days === 'string'
        ? Number(row.max_tokens_per_30_days)
        : typeof row.max_total_tokens === 'number' || typeof row.max_total_tokens === 'string'
          ? Number(row.max_total_tokens)
          : undefined,
    maxTokensPer7Days:
      typeof row.max_tokens_per_7_days === 'number' || typeof row.max_tokens_per_7_days === 'string'
        ? Number(row.max_tokens_per_7_days)
        : typeof row.max_total_tokens === 'number' || typeof row.max_total_tokens === 'string'
          ? Number(row.max_total_tokens)
          : undefined,
    maxTokensPer5Hours:
      typeof row.max_tokens_per_5_hours === 'number' || typeof row.max_tokens_per_5_hours === 'string'
        ? Number(row.max_tokens_per_5_hours)
        : typeof row.max_total_tokens === 'number' || typeof row.max_total_tokens === 'string'
          ? Number(row.max_total_tokens)
          : undefined,
    oneTimeTokenLimit:
      typeof row.one_time_token_limit === 'number' || typeof row.one_time_token_limit === 'string'
        ? Number(row.one_time_token_limit)
        : undefined,
    allowedAppIds: parseAllowedAppIds(row),
    enabled:
      typeof row.enabled === 'boolean' || typeof row.enabled === 'number' || typeof row.enabled === 'string'
        ? normalizeEnabled(row.enabled)
        : undefined,
    updatedAt: typeof row.updated_at === 'string' ? row.updated_at : undefined,
  });
}

async function readTokenSettingsFromTable() {
  await ensureTokenSettingsTable();

  const rows = await db(TOKEN_SETTINGS_TABLE)
    .select(
      'id',
      'name',
      'description',
      'default_expires_in_minutes',
      'max_expires_in_minutes',
      'max_tokens_per_30_days',
      'max_tokens_per_7_days',
      'max_tokens_per_5_hours',
      'one_time_token_limit',
      'max_total_tokens',
      'allowed_app_ids_json',
      'enabled',
      'updated_at',
    )
    .orderBy('name', 'asc');

  return sanitizeTokenSettings(
    rows
      .map((row) => toTokenSettingRecord(row as Record<string, unknown>))
      .filter((item): item is TokenSettingRecord => Boolean(item)),
  );
}

function buildActivityDetail(previous: TokenSettingRecord | null, next: TokenSettingRecord | null) {
  if (!previous && next) {
    return `앱 ${next.allowedAppIds.join(', ') || '-'} / 기본 ${next.defaultExpiresInMinutes}분`;
  }

  if (previous && !next) {
    return `삭제 전 앱 ${previous.allowedAppIds.join(', ') || '-'} / 기본 ${previous.defaultExpiresInMinutes}분`;
  }

  if (!previous || !next) {
    return null;
  }

  const changedFields: string[] = [];
  if (previous.name !== next.name) changedFields.push(`이름 ${previous.name} -> ${next.name}`);
  if (previous.description !== next.description) changedFields.push('설명');
  if (previous.defaultExpiresInMinutes !== next.defaultExpiresInMinutes) changedFields.push(`기본만료 ${previous.defaultExpiresInMinutes} -> ${next.defaultExpiresInMinutes}`);
  if (previous.maxTokensPer30Days !== next.maxTokensPer30Days) changedFields.push(`30일 ${previous.maxTokensPer30Days} -> ${next.maxTokensPer30Days}`);
  if (previous.maxTokensPer7Days !== next.maxTokensPer7Days) changedFields.push(`7일 ${previous.maxTokensPer7Days} -> ${next.maxTokensPer7Days}`);
  if (previous.maxTokensPer5Hours !== next.maxTokensPer5Hours) changedFields.push(`5시간 ${previous.maxTokensPer5Hours} -> ${next.maxTokensPer5Hours}`);
  if (previous.oneTimeTokenLimit !== next.oneTimeTokenLimit) changedFields.push(`1회 ${previous.oneTimeTokenLimit} -> ${next.oneTimeTokenLimit}`);
  if (previous.enabled !== next.enabled) changedFields.push(`사용 ${previous.enabled} -> ${next.enabled}`);
  if (JSON.stringify(previous.allowedAppIds) !== JSON.stringify(next.allowedAppIds)) {
    changedFields.push(`앱 ${previous.allowedAppIds.join(', ') || '-'} -> ${next.allowedAppIds.join(', ') || '-'}`);
  }

  return changedFields.join(' / ') || null;
}

async function replaceTokenSettingsInTable(
  items: TokenSettingRecord[],
  options?: { actorLabel?: string | null; audit?: RequestAuditContext | null },
) {
  await ensureTokenSettingsTable();

  const previousItems = await readTokenSettingsFromTable();
  const nextItems = sanitizeTokenSettings(items);
  const previousById = new Map(previousItems.map((item) => [item.id, item] as const));
  const nextById = new Map(nextItems.map((item) => [item.id, item] as const));

  await db.transaction(async (trx) => {
    await trx(TOKEN_SETTINGS_TABLE).del();

    if (nextItems.length > 0) {
      await trx(TOKEN_SETTINGS_TABLE).insert(
        nextItems.map((item) => ({
          id: item.id,
          name: item.name,
          description: item.description,
          default_expires_in_minutes: item.defaultExpiresInMinutes,
          max_expires_in_minutes: item.maxExpiresInMinutes,
          max_total_tokens: item.maxTokensPer30Days,
          max_tokens_per_30_days: item.maxTokensPer30Days,
          max_tokens_per_7_days: item.maxTokensPer7Days,
          max_tokens_per_5_hours: item.maxTokensPer5Hours,
          one_time_token_limit: item.oneTimeTokenLimit,
          allowed_app_ids_json: JSON.stringify(item.allowedAppIds),
          enabled: item.enabled,
          updated_at: item.updatedAt,
        })),
      );
    }
  });

  const affectedIds = Array.from(new Set([...previousById.keys(), ...nextById.keys()]));

  for (const settingId of affectedIds) {
    const previous = previousById.get(settingId) ?? null;
    const next = nextById.get(settingId) ?? null;

    if (!previous && next) {
      await appendTokenSettingActivity({
        settingId,
        activityType: 'created',
        actorLabel: options?.actorLabel ?? 'manager',
        summary: '토큰 설정을 생성했습니다.',
        detail: buildActivityDetail(previous, next),
        audit: options?.audit,
      });
      continue;
    }

    if (previous && !next) {
      await appendTokenSettingActivity({
        settingId,
        activityType: 'deleted',
        actorLabel: options?.actorLabel ?? 'manager',
        summary: '토큰 설정을 삭제했습니다.',
        detail: buildActivityDetail(previous, next),
        audit: options?.audit,
      });
      continue;
    }

    if (previous && next && JSON.stringify(previous) !== JSON.stringify(next)) {
      await appendTokenSettingActivity({
        settingId,
        activityType: 'updated',
        actorLabel: options?.actorLabel ?? 'manager',
        summary: '토큰 설정을 수정했습니다.',
        detail: buildActivityDetail(previous, next),
        audit: options?.audit,
      });
    }
  }

  return nextItems;
}

export async function getTokenSettingsConfig() {
  return readTokenSettingsFromTable();
}

export async function upsertTokenSettingsConfig(
  items: Partial<TokenSettingRecord>[] | null | undefined,
  options?: { actorLabel?: string | null; audit?: RequestAuditContext | null },
) {
  return replaceTokenSettingsInTable(sanitizeTokenSettings(items), options);
}

export async function getTokenSettingById(id: string) {
  const normalizedId = normalizeSettingId(id);
  if (!normalizedId) {
    return null;
  }

  await ensureTokenSettingsTable();

  const row = await db(TOKEN_SETTINGS_TABLE)
    .where({ id: normalizedId })
    .first(
      'id',
      'name',
      'description',
      'default_expires_in_minutes',
      'max_expires_in_minutes',
      'max_tokens_per_30_days',
      'max_tokens_per_7_days',
      'max_tokens_per_5_hours',
      'one_time_token_limit',
      'max_total_tokens',
      'allowed_app_ids_json',
      'enabled',
      'updated_at',
    );

  if (!row) {
    return null;
  }

  return toTokenSettingRecord(row as Record<string, unknown>);
}
