import { z } from 'zod';
import { db } from '../db/client.js';

export const VISITOR_CLIENT_TABLE = 'visitor_clients';
export const VISITOR_HISTORY_TABLE = 'visitor_visit_histories';

export const trackVisitSchema = z.object({
  clientId: z.string().trim().min(1).max(120),
  url: z.string().trim().min(1).max(2000),
  eventType: z.string().trim().min(1).max(80).default('page_view'),
  userAgent: z.string().trim().max(1000).optional().nullable(),
});

export const listVisitorClientsQuerySchema = z.object({
  limit: z.coerce.number().int().min(1).max(500).optional(),
  search: z.string().trim().max(120).optional(),
  clientId: z.string().trim().max(120).optional(),
  nickname: z.string().trim().max(80).optional(),
  path: z.string().trim().max(500).optional(),
  visitedFrom: z.string().trim().max(40).optional(),
  visitedTo: z.string().trim().max(40).optional(),
});

export const visitorHistoryQuerySchema = z.object({
  limit: z.coerce.number().int().min(1).max(500).optional(),
});

export const updateVisitorNicknameSchema = z.object({
  nickname: z.string().trim().min(1).max(80),
});

export type VisitorClientItem = {
  clientId: string;
  nickname: string;
  firstVisitedAt: string;
  lastVisitedAt: string;
  visitCount: number;
  lastVisitedUrl: string | null;
  lastUserAgent: string | null;
  lastIp: string | null;
  createdAt: string;
  updatedAt: string;
};

export type VisitorHistoryItem = {
  id: number;
  clientId: string;
  visitedAt: string;
  url: string;
  eventType: string;
  userAgent: string | null;
  ip: string | null;
};

function mapVisitorClientRow(row: Record<string, unknown>): VisitorClientItem {
  return {
    clientId: String(row.client_id ?? ''),
    nickname: String(row.nickname ?? ''),
    firstVisitedAt: String(row.first_visited_at ?? ''),
    lastVisitedAt: String(row.last_visited_at ?? ''),
    visitCount: Number(row.visit_count ?? 0),
    lastVisitedUrl: row.last_visited_url ? String(row.last_visited_url) : null,
    lastUserAgent: row.last_user_agent ? String(row.last_user_agent) : null,
    lastIp: row.last_ip ? String(row.last_ip) : null,
    createdAt: String(row.created_at ?? ''),
    updatedAt: String(row.updated_at ?? ''),
  };
}

function mapVisitorHistoryRow(row: Record<string, unknown>): VisitorHistoryItem {
  return {
    id: Number(row.id ?? 0),
    clientId: String(row.client_id ?? ''),
    visitedAt: String(row.visited_at ?? ''),
    url: String(row.url ?? ''),
    eventType: String(row.event_type ?? 'page_view'),
    userAgent: row.user_agent ? String(row.user_agent) : null,
    ip: row.ip ? String(row.ip) : null,
  };
}

function normalizeDateBoundary(value: string, boundary: 'start' | 'end') {
  if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
    return value;
  }

  return boundary === 'start' ? `${value} 00:00:00` : `${value} 23:59:59.999`;
}

async function ensureVisitorClientTable() {
  const hasTable = await db.schema.hasTable(VISITOR_CLIENT_TABLE);

  if (!hasTable) {
    await db.schema.createTable(VISITOR_CLIENT_TABLE, (table) => {
      table.increments('id').primary();
      table.string('client_id', 120).notNullable().unique();
      table.string('nickname', 80).notNullable();
      table.timestamp('first_visited_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
      table.timestamp('last_visited_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
      table.integer('visit_count').notNullable().defaultTo(1);
      table.string('last_visited_url', 2000).nullable();
      table.string('last_user_agent', 1000).nullable();
      table.string('last_ip', 120).nullable();
      table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
      table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
    });

    return;
  }

  const requiredColumns: Array<[string, (table: any) => void]> = [
    ['client_id', (table) => table.string('client_id', 120).notNullable().unique()],
    ['nickname', (table) => table.string('nickname', 80).notNullable().defaultTo('방문자_0001')],
    ['first_visited_at', (table) => table.timestamp('first_visited_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
    ['last_visited_at', (table) => table.timestamp('last_visited_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
    ['visit_count', (table) => table.integer('visit_count').notNullable().defaultTo(1)],
    ['last_visited_url', (table) => table.string('last_visited_url', 2000).nullable()],
    ['last_user_agent', (table) => table.string('last_user_agent', 1000).nullable()],
    ['last_ip', (table) => table.string('last_ip', 120).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(VISITOR_CLIENT_TABLE, columnName);

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

async function ensureVisitorHistoryTable() {
  const hasTable = await db.schema.hasTable(VISITOR_HISTORY_TABLE);

  if (!hasTable) {
    await db.schema.createTable(VISITOR_HISTORY_TABLE, (table) => {
      table.increments('id').primary();
      table.string('client_id', 120).notNullable().index();
      table.timestamp('visited_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
      table.string('url', 2000).notNullable();
      table.string('event_type', 80).notNullable().defaultTo('page_view');
      table.string('user_agent', 1000).nullable();
      table.string('ip', 120).nullable();
    });

    return;
  }

  const requiredColumns: Array<[string, (table: any) => void]> = [
    ['client_id', (table) => table.string('client_id', 120).notNullable().index()],
    ['visited_at', (table) => table.timestamp('visited_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
    ['url', (table) => table.string('url', 2000).notNullable().defaultTo('')],
    ['event_type', (table) => table.string('event_type', 80).notNullable().defaultTo('page_view')],
    ['user_agent', (table) => table.string('user_agent', 1000).nullable()],
    ['ip', (table) => table.string('ip', 120).nullable()],
  ];

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

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

export async function ensureVisitorHistoryTables() {
  await ensureVisitorClientTable();
  await ensureVisitorHistoryTable();
}

async function generateAutoNickname() {
  const result = await db.raw<{ rows?: Array<{ next_nickname_number: number | string | null }> }>(
    `
      select coalesce(max(cast(substring(nickname from '[0-9]+$') as integer)), 0) + 1 as next_nickname_number
      from ${VISITOR_CLIENT_TABLE}
      where nickname like '방문자\\_%'
    `,
  );

  const nextNumber = Number(result.rows?.[0]?.next_nickname_number ?? 1);
  return `방문자_${String(nextNumber).padStart(4, '0')}`;
}

export async function getVisitorClientByClientId(clientId: string) {
  await ensureVisitorHistoryTables();
  const row = await db(VISITOR_CLIENT_TABLE).where({ client_id: clientId }).first();
  return row ? mapVisitorClientRow(row) : null;
}

export type VisitorClientListFilters = {
  search?: string;
  clientId?: string;
  nickname?: string;
  path?: string;
  visitedFrom?: string;
  visitedTo?: string;
};

export async function listVisitorClients(limit = 100, filters: VisitorClientListFilters = {}) {
  await ensureVisitorHistoryTables();

  const query = db(VISITOR_CLIENT_TABLE)
    .select('*')
    .orderBy('last_visited_at', 'desc')
    .limit(Math.min(Math.max(Math.trunc(limit), 1), 500));

  const normalizedSearch = filters.search?.trim() ?? '';
  if (normalizedSearch) {
    query.where((builder) => {
      builder.whereILike('client_id', `%${normalizedSearch}%`).orWhereILike('nickname', `%${normalizedSearch}%`);
    });
  }

  const normalizedClientId = filters.clientId?.trim() ?? '';
  if (normalizedClientId) {
    query.whereILike('client_id', `%${normalizedClientId}%`);
  }

  const normalizedNickname = filters.nickname?.trim() ?? '';
  if (normalizedNickname) {
    query.whereILike('nickname', `%${normalizedNickname}%`);
  }

  const normalizedPath = filters.path?.trim() ?? '';
  const normalizedVisitedFrom = filters.visitedFrom?.trim() ?? '';
  const normalizedVisitedTo = filters.visitedTo?.trim() ?? '';

  if (normalizedPath || normalizedVisitedFrom || normalizedVisitedTo) {
    query.whereIn(
      'client_id',
      db(VISITOR_HISTORY_TABLE)
        .select('client_id')
        .modify((historyQuery) => {
          if (normalizedPath) {
            historyQuery.whereILike('url', `%${normalizedPath}%`);
          }

          if (normalizedVisitedFrom) {
            historyQuery.where('visited_at', '>=', normalizeDateBoundary(normalizedVisitedFrom, 'start'));
          }

          if (normalizedVisitedTo) {
            historyQuery.where('visited_at', '<=', normalizeDateBoundary(normalizedVisitedTo, 'end'));
          }
        }),
    );
  }

  const rows = await query;
  return rows.map((row) => mapVisitorClientRow(row));
}

export async function listVisitorHistories(clientId: string, limit = 100) {
  await ensureVisitorHistoryTables();

  const rows = await db(VISITOR_HISTORY_TABLE)
    .select('*')
    .where({ client_id: clientId })
    .orderBy('visited_at', 'desc')
    .limit(Math.min(Math.max(Math.trunc(limit), 1), 500));

  return rows.map((row) => mapVisitorHistoryRow(row));
}

export async function updateVisitorNickname(clientId: string, nickname: string) {
  await ensureVisitorHistoryTables();

  const rows = await db(VISITOR_CLIENT_TABLE)
    .where({ client_id: clientId })
    .update({
      nickname,
      updated_at: db.fn.now(),
    })
    .returning('*');

  const row = rows[0];
  return row ? mapVisitorClientRow(row) : null;
}

export async function trackVisit(payload: z.infer<typeof trackVisitSchema>, ip: string | null | undefined) {
  await ensureVisitorHistoryTables();
  const parsedPayload = trackVisitSchema.parse(payload);
  const normalizedIp = String(ip ?? '').trim() || null;
  const normalizedUserAgent = parsedPayload.userAgent?.trim() || null;

  const existing = await db(VISITOR_CLIENT_TABLE)
    .select('*')
    .where({ client_id: parsedPayload.clientId })
    .first();

  if (existing) {
    await db(VISITOR_CLIENT_TABLE)
      .where({ client_id: parsedPayload.clientId })
      .update({
        last_visited_at: db.fn.now(),
        visit_count: db.raw('coalesce(visit_count, 0) + 1'),
        last_visited_url: parsedPayload.url,
        last_user_agent: normalizedUserAgent,
        last_ip: normalizedIp,
        updated_at: db.fn.now(),
      });
  } else {
    try {
      // 최초 방문만 자동 닉네임을 발급하고 이후에는 사용자가 수정할 수 있습니다.
      await db(VISITOR_CLIENT_TABLE).insert({
        client_id: parsedPayload.clientId,
        nickname: await generateAutoNickname(),
        first_visited_at: db.fn.now(),
        last_visited_at: db.fn.now(),
        visit_count: 1,
        last_visited_url: parsedPayload.url,
        last_user_agent: normalizedUserAgent,
        last_ip: normalizedIp,
        created_at: db.fn.now(),
        updated_at: db.fn.now(),
      });
    } catch (error) {
      const dbError = error as { code?: string };

      if (dbError.code !== '23505') {
        throw error;
      }

      await db(VISITOR_CLIENT_TABLE)
        .where({ client_id: parsedPayload.clientId })
        .update({
          last_visited_at: db.fn.now(),
          visit_count: db.raw('coalesce(visit_count, 0) + 1'),
          last_visited_url: parsedPayload.url,
          last_user_agent: normalizedUserAgent,
          last_ip: normalizedIp,
          updated_at: db.fn.now(),
        });
    }
  }

  await db(VISITOR_HISTORY_TABLE).insert({
    client_id: parsedPayload.clientId,
    visited_at: db.fn.now(),
    url: parsedPayload.url,
    event_type: parsedPayload.eventType,
    user_agent: normalizedUserAgent,
    ip: normalizedIp,
  });

  return getVisitorClientByClientId(parsedPayload.clientId);
}
