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

const TEXT_MEMO_TABLE = 'text_memo_notes';
const MEMO_LAYOUT_NAME = '메모';
const MAX_NOTE_COUNT = 12;
const MAX_BODY_LENGTH = 1200;
const CLIENT_ID_MAX_LENGTH = 120;
const NOTE_ID_MAX_LENGTH = 120;

const memoBodySchema = z.string().trim().min(1).max(MAX_BODY_LENGTH);
const memoTimestampSchema = z.string().datetime({ offset: true }).optional();

export const textMemoNoteCreateSchema = z.object({
  id: z.string().trim().min(1).max(NOTE_ID_MAX_LENGTH).optional(),
  body: memoBodySchema,
  createdAt: memoTimestampSchema,
  updatedAt: memoTimestampSchema,
});

export const textMemoNoteImportSchema = z.object({
  notes: z.array(
    z.object({
      id: z.string().trim().min(1).max(NOTE_ID_MAX_LENGTH),
      body: memoBodySchema,
      createdAt: z.string().datetime({ offset: true }),
    }),
  ).max(MAX_NOTE_COUNT),
});

export const textMemoNoteUpdateSchema = z.object({
  body: memoBodySchema,
});

export type TextMemoNoteItem = {
  id: string;
  body: string;
  createdAt: string;
  updatedAt: string;
};

function normalizeClientId(value: unknown) {
  return String(value ?? '').trim().slice(0, CLIENT_ID_MAX_LENGTH);
}

function normalizeIsoDate(value: string | undefined, fallback: string) {
  if (!value) {
    return fallback;
  }

  const parsed = Date.parse(value);
  return Number.isNaN(parsed) ? fallback : new Date(parsed).toISOString();
}

function generateNoteId() {
  return `memo-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
}

function mapTextMemoRow(row: Record<string, unknown>): TextMemoNoteItem {
  return {
    id: String(row.note_id ?? ''),
    body: String(row.body ?? ''),
    createdAt: new Date(String(row.created_at ?? '')).toISOString(),
    updatedAt: new Date(String(row.updated_at ?? '')).toISOString(),
  };
}

async function trimClientNotes(clientId: string) {
  const rows = await db(TEXT_MEMO_TABLE)
    .select('note_id')
    .where({ client_id: clientId })
    .orderBy('updated_at', 'desc')
    .orderBy('id', 'desc')
    .offset(MAX_NOTE_COUNT);

  const deleteIds = rows
    .map((row) => String(row.note_id ?? '').trim())
    .filter(Boolean);

  if (deleteIds.length === 0) {
    return;
  }

  await db(TEXT_MEMO_TABLE)
    .where({ client_id: clientId })
    .whereIn('note_id', deleteIds)
    .delete();
}

export async function ensureTextMemoTable() {
  const hasTable = await db.schema.hasTable(TEXT_MEMO_TABLE);

  if (!hasTable) {
    await db.schema.createTable(TEXT_MEMO_TABLE, (table) => {
      table.increments('id').primary();
      table.string('client_id', CLIENT_ID_MAX_LENGTH).notNullable().index();
      table.string('note_id', NOTE_ID_MAX_LENGTH).notNullable();
      table.text('body').notNullable();
      table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
      table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
      table.unique(['client_id', 'note_id']);
    });

    return;
  }

  const requiredColumns: Array<[string, (table: any) => void]> = [
    ['client_id', (table) => table.string('client_id', CLIENT_ID_MAX_LENGTH).notNullable().index()],
    ['note_id', (table) => table.string('note_id', NOTE_ID_MAX_LENGTH).notNullable()],
    ['body', (table) => table.text('body').notNullable().defaultTo('')],
    ['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(TEXT_MEMO_TABLE, columnName);

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

export async function listTextMemoNotes(rawClientId: unknown) {
  const clientId = normalizeClientId(rawClientId);

  if (!clientId) {
    throw new Error('메모를 조회할 clientId가 비어 있습니다.');
  }

  await ensureTextMemoTable();

  const rows = await db(TEXT_MEMO_TABLE)
    .select('*')
    .where({ client_id: clientId })
    .orderBy('updated_at', 'desc')
    .orderBy('id', 'desc')
    .limit(MAX_NOTE_COUNT);

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

export async function createTextMemoNote(rawClientId: unknown, payload: z.infer<typeof textMemoNoteCreateSchema>) {
  const clientId = normalizeClientId(rawClientId);

  if (!clientId) {
    throw new Error('메모를 저장할 clientId가 비어 있습니다.');
  }

  await ensureTextMemoTable();

  const now = new Date().toISOString();
  const createdAt = normalizeIsoDate(payload.createdAt, now);
  const updatedAt = normalizeIsoDate(payload.updatedAt, createdAt);
  const noteId = payload.id?.trim() || generateNoteId();

  await db(TEXT_MEMO_TABLE)
    .insert({
      client_id: clientId,
      note_id: noteId,
      body: payload.body,
      created_at: createdAt,
      updated_at: updatedAt,
    });

  await trimClientNotes(clientId);

  const row = await db(TEXT_MEMO_TABLE)
    .select('*')
    .where({
      client_id: clientId,
      note_id: noteId,
    })
    .first();

  if (!row) {
    throw new Error('저장된 메모를 다시 불러오지 못했습니다.');
  }

  return mapTextMemoRow(row);
}

export async function updateTextMemoNote(
  rawClientId: unknown,
  rawNoteId: unknown,
  payload: z.infer<typeof textMemoNoteUpdateSchema>,
) {
  const clientId = normalizeClientId(rawClientId);
  const noteId = String(rawNoteId ?? '').trim().slice(0, NOTE_ID_MAX_LENGTH);

  if (!clientId || !noteId) {
    throw new Error('수정할 메모 식별자가 올바르지 않습니다.');
  }

  await ensureTextMemoTable();

  const affected = await db(TEXT_MEMO_TABLE)
    .where({
      client_id: clientId,
      note_id: noteId,
    })
    .update({
      body: payload.body,
      updated_at: new Date().toISOString(),
    });

  if (!affected) {
    return null;
  }

  const row = await db(TEXT_MEMO_TABLE)
    .select('*')
    .where({
      client_id: clientId,
      note_id: noteId,
    })
    .first();

  return row ? mapTextMemoRow(row) : null;
}

export async function deleteTextMemoNote(rawClientId: unknown, rawNoteId: unknown) {
  const clientId = normalizeClientId(rawClientId);
  const noteId = String(rawNoteId ?? '').trim().slice(0, NOTE_ID_MAX_LENGTH);

  if (!clientId || !noteId) {
    throw new Error('삭제할 메모 식별자가 올바르지 않습니다.');
  }

  await ensureTextMemoTable();

  const affected = await db(TEXT_MEMO_TABLE)
    .where({
      client_id: clientId,
      note_id: noteId,
    })
    .delete();

  return affected > 0;
}

export async function importTextMemoNotes(
  rawClientId: unknown,
  payload: z.infer<typeof textMemoNoteImportSchema>,
) {
  const clientId = normalizeClientId(rawClientId);

  if (!clientId) {
    throw new Error('메모를 가져올 clientId가 비어 있습니다.');
  }

  await ensureTextMemoTable();

  const existingCountRow = await db(TEXT_MEMO_TABLE)
    .where({ client_id: clientId })
    .count<{ count: number | string }[]>({ count: '*' })
    .first();

  const existingCount = Number(existingCountRow?.count ?? 0);
  if (existingCount > 0) {
    return listTextMemoNotes(clientId);
  }

  const sortedNotes = [...payload.notes]
    .sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))
    .slice(0, MAX_NOTE_COUNT);

  if (sortedNotes.length === 0) {
    return [];
  }

  await db.transaction(async (trx) => {
    for (const note of sortedNotes) {
      await trx(TEXT_MEMO_TABLE).insert({
        client_id: clientId,
        note_id: note.id,
        body: note.body,
        created_at: note.createdAt,
        updated_at: note.createdAt,
      });
    }
  });

  return listTextMemoNotes(clientId);
}

export async function updateTextMemoLayoutFeatureDescription() {
  const layoutRecord = await db('play_layouts').select('id', 'tree').where({ name: MEMO_LAYOUT_NAME }).first();

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

  const tree = layoutRecord.tree as
    | {
        root?: unknown;
        interactions?: Array<Record<string, unknown>>;
        interactionMode?: string;
      }
    | null;

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

  const nextInteractions = [
    {
      id: 'memo-sync-first-line',
      sourceLeafId: 'layout-node-2',
      targetLeafId: 'layout-node-1',
      sourceComponent: {
        label: 'Text Memo Widget',
        optionId: 'widget:text-memo-widget:text-memo-widget',
        keywords: ['text-memo-widget', 'memo'],
      },
      targetComponent: {
        label: 'Base Input · Base',
        optionId: 'component:input:input-base',
        keywords: ['input', 'base'],
      },
      title: '메모 첫 줄 연결',
      description: '선택한 메모와 작성 중 본문의 첫 줄을 Primary Pane Base Input 값으로 동기화합니다.',
      implementationNotes:
        '메모 선택, 본문 입력, 저장 완료 시 첫 줄을 추출해 Base Input에 반영합니다. 빈 본문이면 빈 문자열을 유지합니다.',
    },
    {
      id: 'memo-title-commit',
      sourceLeafId: 'layout-node-1',
      targetLeafId: 'layout-node-2',
      sourceComponent: {
        label: 'Base Input · Base',
        optionId: 'component:input:input-base',
        keywords: ['input', 'base'],
      },
      targetComponent: {
        label: 'Text Memo Widget',
        optionId: 'widget:text-memo-widget:text-memo-widget',
        keywords: ['text-memo-widget', 'memo'],
      },
      title: '제목 확정 반영',
      description: 'Primary Pane Base Input에서 Enter 또는 blur로 확정한 값을 메모 첫 줄로 반영합니다.',
      implementationNotes:
        'InputUI 확정 이벤트를 사용해 현재 편집 중 본문의 첫 줄만 교체하고 나머지 줄바꿈/본문은 유지합니다.',
    },
    {
      id: 'memo-crud-api',
      sourceLeafId: 'layout-node-2',
      targetLeafId: 'layout-node-2',
      sourceComponent: {
        label: 'Text Memo Widget',
        optionId: 'widget:text-memo-widget:text-memo-widget',
        keywords: ['text-memo-widget', 'memo'],
      },
      targetComponent: {
        label: 'Text Memo Widget',
        optionId: 'widget:text-memo-widget:text-memo-widget',
        keywords: ['text-memo-widget', 'memo'],
      },
      title: '메모 저장/조회 API',
      description: '메모 목록 조회와 저장, 수정, 삭제를 text-memo API로 처리합니다.',
      implementationNotes:
        'GET /api/text-memo/notes, POST /api/text-memo/notes, PUT /api/text-memo/notes/:noteId, DELETE /api/text-memo/notes/:noteId 를 사용합니다. clientId 헤더 기준으로 사용자별 최근 12개 메모를 유지합니다.',
    },
  ];

  const previousInteractions = Array.isArray(tree.interactions) ? JSON.stringify(tree.interactions) : '';
  const nextInteractionsJson = JSON.stringify(nextInteractions);
  const nextInteractionMode = 'scoped-v2';

  if (previousInteractions === nextInteractionsJson && tree.interactionMode === nextInteractionMode) {
    return false;
  }

  await db('play_layouts')
    .where({ id: layoutRecord.id as string })
    .update({
      tree: {
        ...tree,
        interactions: nextInteractions,
        interactionMode: nextInteractionMode,
      },
    });

  return true;
}
