import { appendClientIdHeader, getOrCreateClientId } from '../../app/main/clientIdentity';
import { getRegisteredAccessToken } from '../../app/main/tokenAccess';

const TEXT_MEMO_API_BASE_URL = import.meta.env.VITE_WORK_SERVER_URL || '/api';
const TEXT_MEMO_API_PATH = '/text-memo/notes';
const TEXT_MEMO_TABLE = 'text_memo_notes';
const TEXT_MEMO_REQUEST_TIMEOUT_MS = 8000;

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

type TextMemoApiListResponse = {
  ok: boolean;
  items: TextMemoNoteRecord[];
};

type TextMemoApiItemResponse = {
  ok: boolean;
  item: TextMemoNoteRecord;
};

type CrudResponse<Row> = {
  ok: boolean;
  rows: Row[];
};

type SchemaTableRow = {
  table_name: string;
};

type RawTextMemoRow = {
  note_id: string;
  body: string;
  created_at_text: string;
  updated_at_text: string;
};

class TextMemoApiError extends Error {
  status: number;

  constructor(message: string, status: number) {
    super(message);
    this.name = 'TextMemoApiError';
    this.status = status;
  }
}

async function requestTextMemoApi<T>(path: string, init?: RequestInit): Promise<T> {
  const headers = appendClientIdHeader(init?.headers);
  const method = init?.method?.toUpperCase() ?? 'GET';
  const hasBody = init?.body !== undefined && init.body !== null;
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), TEXT_MEMO_REQUEST_TIMEOUT_MS);

  if (hasBody && !headers.has('Content-Type')) {
    headers.set('Content-Type', 'application/json');
  }

  const token = getRegisteredAccessToken();
  if (token && !headers.has('X-Access-Token')) {
    headers.set('X-Access-Token', token);
  }

  let response: Response;

  try {
    response = await fetch(`${TEXT_MEMO_API_BASE_URL}${path}`, {
      ...init,
      headers,
      signal: controller.signal,
      cache: init?.cache ?? (method === 'GET' ? 'no-store' : undefined),
    });
  } catch (error) {
    clearTimeout(timeoutId);

    if (error instanceof DOMException && error.name === 'AbortError') {
      throw new TextMemoApiError('메모 서버 응답이 지연됩니다. 잠시 후 다시 시도해 주세요.', 408);
    }

    throw error;
  }

  clearTimeout(timeoutId);

  if (!response.ok) {
    const text = await response.text();

    try {
      const payload = JSON.parse(text) as { message?: string };
      throw new TextMemoApiError(payload.message || '메모 요청에 실패했습니다.', response.status);
    } catch {
      throw new TextMemoApiError(text || '메모 요청에 실패했습니다.', response.status);
    }
  }

  const contentType = response.headers.get('content-type') ?? '';
  if (!contentType.toLowerCase().includes('application/json')) {
    throw new TextMemoApiError('메모 서버 응답 형식이 올바르지 않습니다.', 502);
  }

  return response.json() as Promise<T>;
}

function getRequiredClientId() {
  const clientId = getOrCreateClientId().trim();

  if (!clientId) {
    throw new TextMemoApiError('메모를 저장할 클라이언트 식별자를 만들지 못했습니다.', 500);
  }

  return clientId;
}

function isNotFoundError(error: unknown) {
  return error instanceof TextMemoApiError && error.status === 404;
}

function mapRow(row: RawTextMemoRow): TextMemoNoteRecord {
  return {
    id: row.note_id,
    body: row.body,
    createdAt: row.created_at_text,
    updatedAt: row.updated_at_text,
  };
}

let fallbackSetupPromise: Promise<void> | null = null;

async function ensureFallbackTable() {
  if (!fallbackSetupPromise) {
    fallbackSetupPromise = (async () => {
      const schema = await requestTextMemoApi<{ items: SchemaTableRow[] }>('/schema/tables');
      const hasTable = schema.items.some((item) => item.table_name === TEXT_MEMO_TABLE);

      if (!hasTable) {
        await requestTextMemoApi('/ddl/raw', {
          method: 'POST',
          body: JSON.stringify({
            sql: `
              create table if not exists ${TEXT_MEMO_TABLE} (
                id bigserial primary key,
                client_id varchar(120) not null,
                note_id varchar(120) not null,
                body text not null,
                created_at timestamptz not null default now(),
                updated_at timestamptz not null default now(),
                created_at_text text null,
                updated_at_text text null
              );
              create unique index if not exists ${TEXT_MEMO_TABLE}_client_note_uidx on ${TEXT_MEMO_TABLE} (client_id, note_id);
              create index if not exists ${TEXT_MEMO_TABLE}_client_updated_idx on ${TEXT_MEMO_TABLE} (client_id, updated_at desc);
            `,
          }),
        });
      }

      await requestTextMemoApi('/ddl/raw', {
        method: 'POST',
        body: JSON.stringify({
          sql: `
            alter table ${TEXT_MEMO_TABLE} add column if not exists created_at_text text null;
            alter table ${TEXT_MEMO_TABLE} add column if not exists updated_at_text text null;
          `,
        }),
      });
    })().catch((error) => {
      fallbackSetupPromise = null;
      throw error;
    });
  }

  await fallbackSetupPromise;
}

async function selectFallbackRows() {
  await ensureFallbackTable();
  const clientId = getRequiredClientId();

  const response = await requestTextMemoApi<CrudResponse<RawTextMemoRow>>(`/crud/${TEXT_MEMO_TABLE}/select`, {
    method: 'POST',
    body: JSON.stringify({
      columns: ['note_id', 'body', 'created_at_text', 'updated_at_text'],
      where: [{ field: 'client_id', operator: 'eq', value: clientId }],
      orderBy: [
        { field: 'updated_at', direction: 'desc' },
        { field: 'id', direction: 'desc' },
      ],
      limit: 12,
    }),
  });

  return response.rows.map(mapRow);
}

async function trimFallbackRows() {
  const clientId = getRequiredClientId();
  const response = await requestTextMemoApi<CrudResponse<{ note_id: string }>>(`/crud/${TEXT_MEMO_TABLE}/select`, {
    method: 'POST',
    body: JSON.stringify({
      columns: ['note_id'],
      where: [{ field: 'client_id', operator: 'eq', value: clientId }],
      orderBy: [
        { field: 'updated_at', direction: 'desc' },
        { field: 'id', direction: 'desc' },
      ],
      limit: 500,
      offset: 12,
    }),
  });

  const extraIds = response.rows.map((row) => row.note_id).filter(Boolean);

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

  await requestTextMemoApi(`/crud/${TEXT_MEMO_TABLE}/delete`, {
    method: 'DELETE',
    body: JSON.stringify({
      where: [
        { field: 'client_id', operator: 'eq', value: clientId },
        { field: 'note_id', operator: 'in', value: extraIds },
      ],
    }),
  });
}

async function fetchTextMemoNotesFallback() {
  return selectFallbackRows();
}

async function createTextMemoNoteFallback(payload: {
  id?: string;
  body: string;
  createdAt?: string;
  updatedAt?: string;
}) {
  await ensureFallbackTable();
  const clientId = getRequiredClientId();

  const noteId = payload.id?.trim() || `memo-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
  const now = new Date().toISOString();
  const createdAt = payload.createdAt?.trim() || now;
  const updatedAt = payload.updatedAt?.trim() || createdAt;

  const response = await requestTextMemoApi<CrudResponse<RawTextMemoRow>>(`/crud/${TEXT_MEMO_TABLE}/insert`, {
    method: 'POST',
    body: JSON.stringify({
      data: {
        client_id: clientId,
        note_id: noteId,
        body: payload.body,
        created_at: createdAt,
        updated_at: updatedAt,
        created_at_text: createdAt,
        updated_at_text: updatedAt,
      },
    }),
  });

  await trimFallbackRows();
  return mapRow(response.rows[0]);
}

async function updateTextMemoNoteFallback(noteId: string, payload: { body: string }) {
  await ensureFallbackTable();
  const clientId = getRequiredClientId();

  const response = await requestTextMemoApi<CrudResponse<RawTextMemoRow>>(`/crud/${TEXT_MEMO_TABLE}/update`, {
    method: 'PATCH',
    body: JSON.stringify({
      data: {
        body: payload.body,
        updated_at: new Date().toISOString(),
        updated_at_text: new Date().toISOString(),
      },
      where: [
        { field: 'client_id', operator: 'eq', value: clientId },
        { field: 'note_id', operator: 'eq', value: noteId },
      ],
    }),
  });

  if (!response.rows[0]) {
    throw new TextMemoApiError('수정할 메모를 찾을 수 없습니다.', 404);
  }

  return mapRow(response.rows[0]);
}

async function deleteTextMemoNoteFallback(noteId: string) {
  await ensureFallbackTable();
  const clientId = getRequiredClientId();

  const response = await requestTextMemoApi<CrudResponse<RawTextMemoRow>>(`/crud/${TEXT_MEMO_TABLE}/delete`, {
    method: 'DELETE',
    body: JSON.stringify({
      where: [
        { field: 'client_id', operator: 'eq', value: clientId },
        { field: 'note_id', operator: 'eq', value: noteId },
      ],
    }),
  });

  if ((response.rows?.length ?? 0) === 0) {
    throw new TextMemoApiError('삭제할 메모를 찾을 수 없습니다.', 404);
  }
}

async function importTextMemoNotesFallback(notes: Array<{ id: string; body: string; createdAt: string }>) {
  const existingNotes = await fetchTextMemoNotesFallback();
  if (existingNotes.length > 0) {
    return existingNotes;
  }

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

  for (const note of sortedNotes) {
    await createTextMemoNoteFallback({
      id: note.id,
      body: note.body,
      createdAt: note.createdAt,
      updatedAt: note.createdAt,
    });
  }

  return fetchTextMemoNotesFallback();
}

export async function fetchTextMemoNotes() {
  try {
    const response = await requestTextMemoApi<TextMemoApiListResponse>(TEXT_MEMO_API_PATH);
    return response.items;
  } catch (error) {
    if (!isNotFoundError(error)) {
      throw error;
    }

    return fetchTextMemoNotesFallback();
  }
}

export async function createTextMemoNote(payload: {
  id?: string;
  body: string;
  createdAt?: string;
  updatedAt?: string;
}) {
  try {
    const response = await requestTextMemoApi<TextMemoApiItemResponse>(TEXT_MEMO_API_PATH, {
      method: 'POST',
      body: JSON.stringify(payload),
    });

    return response.item;
  } catch (error) {
    if (!isNotFoundError(error)) {
      throw error;
    }

    return createTextMemoNoteFallback(payload);
  }
}

export async function updateTextMemoNote(noteId: string, payload: { body: string }) {
  try {
    const response = await requestTextMemoApi<TextMemoApiItemResponse>(
      `${TEXT_MEMO_API_PATH}/${encodeURIComponent(noteId)}`,
      {
        method: 'PUT',
        body: JSON.stringify(payload),
      },
    );

    return response.item;
  } catch (error) {
    if (!isNotFoundError(error)) {
      throw error;
    }

    return updateTextMemoNoteFallback(noteId, payload);
  }
}

export async function deleteTextMemoNote(noteId: string) {
  try {
    await requestTextMemoApi<{ ok: boolean }>(`${TEXT_MEMO_API_PATH}/${encodeURIComponent(noteId)}`, {
      method: 'DELETE',
    });
  } catch (error) {
    if (!isNotFoundError(error)) {
      throw error;
    }

    await deleteTextMemoNoteFallback(noteId);
  }
}

export async function importTextMemoNotes(notes: Array<{ id: string; body: string; createdAt: string }>) {
  try {
    const response = await requestTextMemoApi<TextMemoApiListResponse>(`${TEXT_MEMO_API_PATH}/import`, {
      method: 'POST',
      body: JSON.stringify({ notes }),
    });

    return response.items;
  } catch (error) {
    if (!isNotFoundError(error)) {
      throw error;
    }

    return importTextMemoNotesFallback(notes);
  }
}
