import type { Knex } from 'knex';
import type { FastifyInstance, FastifyRequest } from 'fastify';
import { z } from 'zod';
import { db } from '../db/client.js';
import { assertIdentifier } from '../lib/identifier.js';

const filterSchema = z.object({
  field: z.string(),
  operator: z
    .enum(['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'like', 'in', 'null', 'notNull'])
    .default('eq'),
  value: z.any().optional(),
});

const orderBySchema = z.object({
  field: z.string(),
  direction: z.enum(['asc', 'desc']).default('asc'),
});

const selectSchema = z.object({
  columns: z.array(z.string()).optional(),
  where: z.array(filterSchema).optional(),
  orderBy: z.array(orderBySchema).optional(),
  limit: z.number().int().positive().max(500).optional(),
  offset: z.number().int().min(0).optional(),
});

const insertSchema = z.object({
  data: z.record(z.string(), z.any()).or(z.array(z.record(z.string(), z.any()))),
});

const updateSchema = z.object({
  data: z.record(z.string(), z.any()),
  where: z.array(filterSchema).default([]),
});

const deleteSchema = z.object({
  where: z.array(filterSchema).default([]),
});

const protectedBoardPostAutomationFields = new Set(['automation_plan_item_id', 'automation_received_at']);
const secretFieldPattern = /(?:^|_|-)(?:password|passwd|pwd|passcode|secret|token|api[_-]?key|access[_-]?key|private[_-]?key)(?:$|_|-)/i;
const loginFieldPattern =
  /(?:^|_|-)(?:login[_-]?id|login[_-]?name|username|user[_-]?name|user[_-]?id|account[_-]?id|account[_-]?name|admin[_-]?id|admin[_-]?name|member[_-]?id|member[_-]?name|email)(?:$|_|-)/i;

function maskCredentialValue(value: string) {
  const trimmed = value.trim();

  if (!trimmed) {
    return value;
  }

  if (trimmed.length <= 2) {
    return '*'.repeat(trimmed.length);
  }

  if (trimmed.length <= 4) {
    return `${trimmed[0]}${'*'.repeat(trimmed.length - 2)}${trimmed.at(-1) ?? ''}`;
  }

  return `${trimmed.slice(0, 2)}${'*'.repeat(Math.max(2, trimmed.length - 4))}${trimmed.slice(-2)}`;
}

function shouldMaskLoginField(fieldName: string, row: Record<string, unknown>) {
  if (!loginFieldPattern.test(fieldName)) {
    return false;
  }

  return Object.keys(row).some((candidateField) => secretFieldPattern.test(candidateField));
}

export function maskCrudRowSensitiveFields<T>(value: T): T {
  if (Array.isArray(value)) {
    return value.map((item) => maskCrudRowSensitiveFields(item)) as T;
  }

  if (!value || typeof value !== 'object') {
    return value;
  }

  const row = value as Record<string, unknown>;
  const result: Record<string, unknown> = {};

  Object.entries(row).forEach(([fieldName, fieldValue]) => {
    if (typeof fieldValue === 'string') {
      if (secretFieldPattern.test(fieldName) || shouldMaskLoginField(fieldName, row)) {
        result[fieldName] = maskCredentialValue(fieldValue);
        return;
      }
    }

    result[fieldName] = maskCrudRowSensitiveFields(fieldValue);
  });

  return result as T;
}

function applyFilters(query: Knex.QueryBuilder, filters: z.infer<typeof filterSchema>[] = []) {
  filters.forEach((filter) => {
    const field = assertIdentifier(filter.field, 'field');

    switch (filter.operator) {
      case 'eq':
        query.where(field, filter.value);
        break;
      case 'ne':
        query.whereNot(field, filter.value);
        break;
      case 'gt':
        query.where(field, '>', filter.value);
        break;
      case 'gte':
        query.where(field, '>=', filter.value);
        break;
      case 'lt':
        query.where(field, '<', filter.value);
        break;
      case 'lte':
        query.where(field, '<=', filter.value);
        break;
      case 'like':
        query.where(field, 'like', filter.value);
        break;
      case 'in':
        query.whereIn(field, Array.isArray(filter.value) ? filter.value : [filter.value]);
        break;
      case 'null':
        query.whereNull(field);
        break;
      case 'notNull':
        query.whereNotNull(field);
        break;
      default:
        break;
    }
  });
}

export async function registerCrudRoutes(app: FastifyInstance) {
  function getRequestTraceContext(request: FastifyRequest) {
    return {
      ip: request.ip,
      remoteAddress: request.raw.socket.remoteAddress,
      host: request.headers.host,
      origin: request.headers.origin,
      referer: request.headers.referer,
      userAgent: request.headers['user-agent'],
      clientId: request.headers['x-client-id'],
    };
  }

  function summarizeCrudUpdatePayload(table: string, payload: z.infer<typeof updateSchema>) {
    if (table !== 'board_posts') {
      return {
        dataKeys: Object.keys(payload.data),
        where: payload.where,
      };
    }

    return {
      dataKeys: Object.keys(payload.data),
      where: payload.where,
      automationPlanItemId: payload.data.automation_plan_item_id ?? null,
      automationReceivedAt: payload.data.automation_received_at ?? null,
      title: typeof payload.data.title === 'string' ? payload.data.title : undefined,
      contentLength: typeof payload.data.content === 'string' ? payload.data.content.length : undefined,
    };
  }

  app.post('/api/crud/:table/select', async (request) => {
    const table = assertIdentifier((request.params as { table: string }).table, 'table name');
    const payload = selectSchema.parse(request.body ?? {});
    const columns = payload.columns?.map((column) => assertIdentifier(column, 'column')) ?? ['*'];

    const query = db(table).select(columns);
    applyFilters(query, payload.where);

    payload.orderBy?.forEach((order) => {
      query.orderBy(assertIdentifier(order.field, 'order field'), order.direction);
    });

    if (payload.limit) {
      query.limit(payload.limit);
    }

    if (payload.offset) {
      query.offset(payload.offset);
    }

    const rows = await query;

    return {
      ok: true,
      table,
      count: rows.length,
      rows: maskCrudRowSensitiveFields(rows),
    };
  });

  app.post('/api/crud/:table/insert', async (request) => {
    const table = assertIdentifier((request.params as { table: string }).table, 'table name');
    const payload = insertSchema.parse(request.body);
    const inserted = await db(table).insert(payload.data).returning('*');

    return {
      ok: true,
      table,
      rows: maskCrudRowSensitiveFields(inserted),
    };
  });

  app.patch('/api/crud/:table/update', async (request, reply) => {
    const table = assertIdentifier((request.params as { table: string }).table, 'table name');
    const payload = updateSchema.parse(request.body);
    const query = db(table);

    applyFilters(query, payload.where);

    if (table === 'board_posts') {
      request.log.warn(
        {
          table,
          payload: summarizeCrudUpdatePayload(table, payload),
          trace: getRequestTraceContext(request),
        },
        'Board post CRUD update requested',
      );

      const protectedFields = Object.keys(payload.data).filter((field) => protectedBoardPostAutomationFields.has(field));

      if (protectedFields.length) {
        request.log.warn(
          {
            table,
            protectedFields,
            payload: summarizeCrudUpdatePayload(table, payload),
            trace: getRequestTraceContext(request),
          },
          'Board post CRUD update blocked from changing automation link fields',
        );

        return reply.code(409).send({
          message: '자동화 접수 연결 필드는 일반 CRUD 수정으로 변경할 수 없습니다.',
          protectedFields,
        });
      }
    }

    const rows = await query.update(payload.data).returning('*');

    return {
      ok: true,
      table,
      count: rows.length,
      rows: maskCrudRowSensitiveFields(rows),
    };
  });

  app.delete('/api/crud/:table/delete', async (request) => {
    const table = assertIdentifier((request.params as { table: string }).table, 'table name');
    const payload = deleteSchema.parse(request.body ?? {});
    const query = db(table);

    applyFilters(query, payload.where);

    const rows = await query.delete().returning('*');

    return {
      ok: true,
      table,
      count: rows.length,
      rows: maskCrudRowSensitiveFields(rows),
    };
  });
}
