// src/server/features/notes/notes.service.ts
// Business logic for notes management

import crypto from 'node:crypto';
import { db } from '../../shared/db/client';
import { notes } from './db/notes.table';
import { noteComments } from './db/note_comments.table';
import { noteLayouts } from './db/note_layouts.table';
import { loggerService } from '../../shared/lib/logger';
import { createExtendedBaseService } from '../../shared/db/base.service';
import { sanitizeHtml } from '../../shared/lib/sanitize';
import type { UserInfo } from '../../shared/lib/auth';
import { USER_ROLES } from '@shared/constants/roles';
import { eq, desc, and, like, and as and_ } from 'drizzle-orm';
import { EVENTS } from '@shared/contracts/events';
import { dayjs } from '../../shared/lib/dayjs';
import {
  findWithLayout,
  findOneWithLayout,
  updateNoteLayout,
  findComments,
  findHistory,
  hardDeleteNote,
  hardDeleteNoteLayouts,
  findLayout,
} from './db/notes.repository';
import { exportNotesToCSV } from './lib/export.service';
import { createCommentHistory, createPriorityChangeHistory } from './lib/history.service';
import { resolveNoteCollisions, handleCoordinateUpdate, findBestPosition, distributeNoteToAllUsers } from './lib/layout.service';
import {
  handleContentUpdate,
  handleManagerRestrictedUpdate,
} from './lib/history.service';
import {
  emitNoteCreatedEvent,
  emitNoteDeletedEvent,
  emitCommentCreatedEvent,
} from './notes.events';
import { deleteNotificationsByNoteId, deleteAllNotificationsByNoteId } from '../notifications/notifications.service';

// Create base services
const notesBaseService = createExtendedBaseService(notes);
const commentsBaseService = createExtendedBaseService(noteComments);
const notesLogger = loggerService.get('DB');

export interface CreateNoteInput {
  id?: string;
  title: string;
  content: string;
  authorId: string;
  userId: string;
  spaceId: string;
  priority?: 'low' | 'normal' | 'high';
  isPublic?: boolean;
  status?: 'active' | 'done' | 'cancelled';
  height?: number; // Используется только в note_layouts, не в основной таблице notes
  reminderAt?: Date | null;
}

export interface UpdateNoteInput {
  title?: string;
  content?: string;
  priority?: 'low' | 'normal' | 'high';
  status?: 'active' | 'done' | 'cancelled';
  isPublic?: boolean;
  height?: number; // Используется только в note_layouts, не в основной таблице notes
  userId?: string;
  spaceId?: string;
  comment?: string;
  reminderAt?: Date | null;
  priorityUpdatedAt?: Date | null;
  lastPriorityAuthorId?: string;
}

export interface CreateNoteCommentInput {
  noteId: string;
  content: string;
  authorId: string;
}

/**
 * Get the last mention timestamp for a user in a note
 * Searches both note content and comments for <app-mention user-id="UUID"> pattern
 * Returns ISO string of the latest mention date or null if no mentions found
 */
export async function getNoteMentions(noteId: string, userId: string): Promise<string | null> {
  const mentionPattern = `%user-id="${userId}"%`;
  
  // Check note content for mentions
  const noteResult = await db
    .select({ date: notes.createdAt })
    .from(notes)
    .where(and(eq(notes.id, noteId), like(notes.content, mentionPattern)))
    .limit(1);
  
  // Check comments for mentions (get the latest one)
  const commentResult = await db
    .select({ date: noteComments.createdAt })
    .from(noteComments)
    .where(and(eq(noteComments.noteId, noteId), like(noteComments.content, mentionPattern)))
    .orderBy(desc(noteComments.createdAt))
    .limit(1);
  
  // Get the maximum date from both results
  const dates: Date[] = [];
  if (noteResult.length > 0 && noteResult[0].date) {
    dates.push(noteResult[0].date);
  }
  if (commentResult.length > 0 && commentResult[0].date) {
    dates.push(commentResult[0].date);
  }
  
  if (dates.length === 0) {
    return null;
  }
  
  // Return the latest date as ISO string
  const latestDate = dates.reduce((max, date) => date > max ? date : max, dates[0]);
  return dayjs.utc(latestDate).toISOString();
}

// Get all active notes (not archived) for a specific user
export async function getNotes(userId: string) {
  const result = await findWithLayout(userId);
  notesLogger.info(`getNotes: userId=${userId}, totalNotes=${result.length}`);
  return result;
}

// Get a single note by ID (only if not archived)
export async function getNoteById(id: string) {
  const result = await notesBaseService.findById(id);
  if (!result) throw new Error('Note not found');
  return result as any;
}

// Get a single note by ID with personal layout coordinates for a specific user
export async function getNoteByIdWithLayout(id: string, userId: string) {
  const result = await findOneWithLayout(id, userId);
  if (!result) throw new Error('Note not found');
  return result;
}

// Create a new note
export async function createNote(input: CreateNoteInput) {
  try {
    notesLogger.debug('[DEBUG] Input to createNote:', JSON.stringify(input, null, 2));
    const noteId = input.id || crypto.randomUUID();
    const sanitizedTitle = sanitizeHtml(input.title);
    const sanitizedContent = sanitizeHtml(input.content);

    notesLogger.debug('[DEBUG] Calling findBestPosition...');
    // Find the best position using Smart Masonry Layout
    // Pass noteId to exclude it from calculation (to avoid self-intersection)
    const bestPosition = await findBestPosition(input.userId, 6, noteId);
    notesLogger.debug('[DEBUG] Best position:', bestPosition);

    // ВАЖНО: Мы доверяем алгоритму, а не фронтенду (который шлет дефолтные 1 и 0)
    // Если пользователь просто нажал "Создать", мы должны сами найти место.
    const targetColumn = bestPosition.column;
    const targetPositionY = bestPosition.positionY;

    notesLogger.debug('[DEBUG] Calling resolveNoteCollisions...');
    // Resolve Collisions (на всякий случай, если там кто-то успел влезть)
    const resolvedPosition = await resolveNoteCollisions(
      noteId,
      targetColumn, // ИСПОЛЬЗУЕМ РАСЧЕТНУЮ КОЛОНКУ!
      targetPositionY,
      input.height ?? 220,
      input.userId,
      input.status ?? 'active'
    );

    notesLogger.debug('[DEBUG] Resolved position:', resolvedPosition);

    const payload = {
      id: noteId,
      title: sanitizedTitle,
      content: sanitizedContent,
      authorId: input.authorId,
      userId: input.userId,
      spaceId: input.spaceId,
      priority: input.priority ?? 'normal',
      isPublic: input.isPublic ?? false,
      status: input.status ?? 'active',
      // Обязательные поля для индикаторов:
      priorityUpdatedAt: new Date(),
      lastPriorityAuthorId: input.authorId,
      lastCommentAt: null,
    };

    notesLogger.debug('[DEBUG] Creation Payload:', payload);

    notesLogger.debug('[DEBUG] Calling notesBaseService.create...');
    const createdNote = await notesBaseService.create(payload);
    notesLogger.debug('[DEBUG] notesBaseService.create returned:', createdNote);

    notesLogger.debug('[DEBUG] Calling updateNoteLayout...');
    // Layout для автора
    await updateNoteLayout(
      noteId,
      input.userId,
      targetColumn, // СЕРВЕРНАЯ КОЛОНКА
      resolvedPosition.positionY, // СЕРВЕРНАЯ ПОЗИЦИЯ
      input.height ?? 220,
      input.reminderAt
    );

    notesLogger.debug('[DEBUG] updateNoteLayout completed');
    notesLogger.debug('[DEBUG] Calling emitNoteCreatedEvent...');
    // Transform createdNote to match contract with ISO string dates
    const noteForEvent = {
      id: noteId,
      title: sanitizedTitle,
      content: sanitizedContent,
      authorId: input.authorId,
      userId: input.userId,
      spaceId: input.spaceId,
      priority: input.priority ?? 'normal',
      status: input.status ?? 'active',
      isPublic: input.isPublic ?? false,
      createdAt: dayjs.utc((createdNote as any).createdAt).toISOString(),
      updatedAt: dayjs.utc((createdNote as any).updatedAt).toISOString(),
      archivedAt: null,
      lastCommentAt: null,
      lastCommentAuthorId: null,
      priorityUpdatedAt: dayjs.utc((createdNote as any).priorityUpdatedAt).toISOString(),
      lastPriorityAuthorId: input.authorId,
      lastMentionAt: null,
      layout: {
        id: crypto.randomUUID(),
        noteId: noteId,
        userId: input.userId,
        column: targetColumn,
        positionY: resolvedPosition.positionY,
        height: input.height ?? 220,
        reminderAt: input.reminderAt ? dayjs.utc(input.reminderAt).toISOString() : null,
        lastViewedAt: null,
        createdAt: dayjs.utc((createdNote as any).createdAt).toISOString(),
        updatedAt: dayjs.utc((createdNote as any).updatedAt).toISOString(),
      },
      statusChangeReason: null,
    };
    await emitNoteCreatedEvent({
      noteId,
      note: noteForEvent,
      authorId: input.authorId,
      content: sanitizedContent,
    });

    notesLogger.debug('[DEBUG] emitNoteCreatedEvent completed');

    // Distribute note to ALL users if it's public
    if (input.isPublic === true) {
      notesLogger.debug('[DEBUG] Starting distribution...');
      try {
        await distributeNoteToAllUsers(
          noteId,
          input.height ?? 220,
          input.authorId
        );
      } catch (distError) {
        // Log the FULL error to see why it fails
        notesLogger.error('[CRITICAL] Layout distribution FAILED:', distError);
      }
    }

    notesLogger.info(`Note created: ${noteId} at Col ${targetColumn}, Y ${resolvedPosition.positionY}`);
    return noteId;
  } catch (err) {
    notesLogger.error('[CRITICAL] Note Creation Failed:', err);
    notesLogger.error('[CRITICAL] Error stack:', err instanceof Error ? err.stack : 'No stack available');
    throw err; // Пробрасываем в глобальный хендлер
  }
}

// Update a note with isPublic Guard
export async function updateNote(id: string, input: UpdateNoteInput, user?: UserInfo) {
  notesLogger.debug('[DEBUG] updateNote called with id:', id, 'input:', input, 'user:', user?.id);
  
  const existingNote = await getNoteById(id);
  notesLogger.debug('[DEBUG] updateNote: existingNote.status:', existingNote.status);
  notesLogger.debug('[DEBUG] updateNote: input.status:', input.status);
  
  const sanitizedTitle = input.title !== undefined ? sanitizeHtml(input.title) : undefined;
  const sanitizedContent = input.content !== undefined ? sanitizeHtml(input.content) : undefined;

  // Get comments for validation
  const comments = await findComments(id);
  const isAuthor = user?.id === existingNote.authorId;
  const hasComments = comments.length > 0;

  // Check if this is a status-only update (with optional comment)
  // More reliable check: status-only if there's no title/content change
  const isStatusOnlyUpdate = !input.title && !input.content && input.status;

  // Check if isPublic field is changing
  const isPublicChanged = input.isPublic !== undefined && input.isPublic !== existingNote.isPublic;

  // Check for actual content changes (not just undefined values)
  const isTitleDifferent = sanitizedTitle !== undefined && sanitizedTitle !== existingNote.title;
  const isContentDifferent = sanitizedContent !== undefined && sanitizedContent !== existingNote.content;
  const isChangingContent = isTitleDifferent || isContentDifferent;

  // Guard: Comment is required when changing status
  if (input.status && input.status !== existingNote.status) {
    if (!input.comment || input.comment.trim() === '') {
      throw new Error('Необходимо указать причину изменения статуса');
    }
  }

  // Manager validation for public notes
  if (existingNote.isPublic && user?.role === USER_ROLES.MANAGER) {
    // BYPASS: Allow status-only updates regardless of comments
    if (!isStatusOnlyUpdate) {
      // 1. Проверка попытки скрыть заметку (isPublic: true -> false)
      if (input.isPublic === false) {
        if (!isAuthor) {
          throw new Error('Только автор может вернуть заметку в личные');
        }
        if (hasComments) {
          throw new Error('Нельзя скрыть заметку, в которой уже есть комментарии');
        }
        // Если автор и нет комментов - разрешаем смену isPublic
      }

      // 2. Проверка редактирования контента (title или content)
      // Используем уже вычисленный флаг isChangingContent, который проверяет реальные изменения
      if (isChangingContent) {
        if (hasComments) {
          throw new Error('Редактирование запрещено: в заметке есть комментарии');
        }
        if (!isAuthor) {
          throw new Error('Только автор может редактировать текст публичной заметки');
        }
      }
    }
  }

  // Handle public transition: distribute note to all users without recalculating author's position
  if (input.isPublic === true && !existingNote.isPublic && user) {
    notesLogger.debug('[DEBUG] Transitioning note to public, distributing to ALL users...');
    // NOTE: We do NOT recalculate position for the author here.
    // input.column and input.positionY remain undefined (unless user moved the card).
    // This allows handleCoordinateUpdate (called below) to preserve the author's current coordinates from note_layouts.
    
    // Get author's layout to use their height for distribution
    const authorLayout = await findLayout(id, existingNote.authorId);
    const distributionHeight = input.height ?? authorLayout?.height ?? 220;
    
    // Distribute note to ALL users with personalized layouts
    try {
      await distributeNoteToAllUsers(
        id,
        distributionHeight,
        existingNote.authorId
      );
    } catch (distError) {
      notesLogger.error('[CRITICAL] Layout distribution FAILED:', distError);
    }
    notesLogger.debug('[DEBUG] Distribution completed');
  }

  // Обработка перехода в «Активные» (Восстановление координат)
  // Если заметку возвращают из "Выполненные/Отмененные" в "Активные"
  if (input.status === 'active' && existingNote.status !== 'active' && user) {
    notesLogger.debug('[LAYOUT] Restoring note to active board, calculating new position...');
    
    // Get user's layout to use their height for restoration
    const userLayout = await findLayout(id, user.id);
    const restorationHeight = input.height ?? userLayout?.height ?? 220;
    
    // Используем ту же математику, что и при создании
    const bestPosition = await findBestPosition(user.id, 6, id);
    
    const resolvedPosition = await resolveNoteCollisions(
      id,
      bestPosition.column,
      bestPosition.positionY,
      restorationHeight,
      user.id,
      'active'
    );

    // Сохраняем новые координаты в layouts
    await updateNoteLayout(
      id,
      user.id,
      bestPosition.column,
      resolvedPosition.positionY,
      restorationHeight
    );

    // Если заметка публичная — раздаем всем новые координаты
    if (existingNote.isPublic) {
      await distributeNoteToAllUsers(id, restorationHeight, existingNote.authorId);
    }

    notesLogger.info(`[LAYOUT] Note ${id} restored to active board at Col ${bestPosition.column}, Y ${resolvedPosition.positionY}`);
  }

  // Обработка ухода из «Активных» (Очистка координат)
  // Если заметка уходит с доски, удаляем её координаты, чтобы не мешала другим
  if (input.status && input.status !== 'active' && existingNote.status === 'active') {
    notesLogger.debug('[LAYOUT] Note inactivated, clearing layouts for id:', id);
    await hardDeleteNoteLayouts(id);
    notesLogger.info(`[LAYOUT] Note ${id} layouts cleared (status changed from active to ${input.status})`);
  }

  // Проверяем изменение приоритета и устанавливаем priorityUpdatedAt и lastPriorityAuthorId
  const priorityChanged = input.priority !== undefined && input.priority !== existingNote.priority;
  if (priorityChanged) {
    notesLogger.debug('[DEBUG] Priority changed from', existingNote.priority, 'to', input.priority);
    // Принудительно устанавливаем priorityUpdatedAt как Date объект для Drizzle
    input.priorityUpdatedAt = new Date();
    // Сохраняем ID автора изменения приоритета (для исключения автора из индикатора)
    if (user) {
      input.lastPriorityAuthorId = user.id;
    }
  }

  if (user) await handleCoordinateUpdate(id, input, existingNote, user);

  // Early return if no actual changes (no content, status, priority, or isPublic changes)
  if (!isChangingContent && !priorityChanged && !isStatusOnlyUpdate && !isPublicChanged) {
    notesLogger.debug('[DEBUG] updateNote: No actual changes detected, returning success');
    return { success: true };
  }

  await handleContentUpdate(id, input, existingNote, sanitizedTitle, sanitizedContent, user, priorityChanged);
  notesLogger.debug('updateNote: Note updated successfully, new status should be:', input.status || existingNote.status);
  notesLogger.info(`Note updated: ${id}`);
  
  // If note was changed from public to private, revoke creation notifications for all except author
  if (existingNote.isPublic && input.isPublic === false) {
    try {
      await deleteNotificationsByNoteId(id, EVENTS.NOTE_CREATED, existingNote.authorId);
      notesLogger.info(`Revoked note:created notifications for hidden note ${id}`);
    } catch (error) {
      notesLogger.error(`Failed to revoke notifications for hidden note ${id}:`, error);
      // Don't throw - note update was successful
    }
  }
  
  return { success: true };
}

// Hard delete a note (physically remove from database) with permission validation
export async function deleteNote(id: string, user: UserInfo) {
  const existingNote = await getNoteById(id);
  
  // Admin can delete any note
  if (user.role === USER_ROLES.ADMIN) {
    await hardDeleteNote(id);
    await deleteAllNotificationsByNoteId(id);
    await emitNoteDeletedEvent({ noteId: id });
    notesLogger.info(`Note hard deleted by admin: ${id}`);
    return;
  }
  
  // Author can delete their own note
  if (user.id === existingNote.authorId) {
    await hardDeleteNote(id);
    await deleteAllNotificationsByNoteId(id);
    await emitNoteDeletedEvent({ noteId: id });
    notesLogger.info(`Note hard deleted by author: ${id}`);
    return;
  }
  
  // Manager can delete public notes only if there are no comments
  if (user.role === USER_ROLES.MANAGER && existingNote.isPublic) {
    const comments = await findComments(id);
    if (comments.length > 0) {
      throw new Error('Нельзя удалить публичную заметку, в которой есть обсуждение');
    }
    await hardDeleteNote(id);
    await deleteAllNotificationsByNoteId(id);
    await emitNoteDeletedEvent({ noteId: id });
    notesLogger.info(`Public note hard deleted by manager: ${id}`);
    return;
  }
  
  // If none of the above conditions are met, deny deletion
  throw new Error('У вас нет прав для удаления этой заметки');
}

// Get comments for a note
export async function getNoteComments(noteId: string) {
  return await findComments(noteId);
}

// Create a comment for a note
export async function createNoteComment(input: CreateNoteCommentInput) {
  notesLogger.debug('[DEBUG] createNoteComment called with input:', input);
  const sanitizedContent = sanitizeHtml(input.content);
  const commentId = crypto.randomUUID();

  notesLogger.debug('[DEBUG] Creating comment with data:', {
    id: commentId,
    noteId: input.noteId,
    authorId: input.authorId,
    content: sanitizedContent,
    isRead: false,
  });

  const createdComment = await commentsBaseService.create({
    id: commentId,
    noteId: input.noteId,
    authorId: input.authorId,
    content: sanitizedContent,
    isRead: false,
  });

  // FIXED: Update notes table with lastCommentAt and lastCommentAuthorId
  await db
    .update(notes)
    .set({
      lastCommentAt: new Date(),
      lastCommentAuthorId: input.authorId,
    })
    .where(eq(notes.id, input.noteId));

  notesLogger.debug('[DEBUG] createdComment from DB:', createdComment);
  notesLogger.debug('[DEBUG] createdComment.createdAt type:', typeof createdComment.createdAt);
  notesLogger.debug('[DEBUG] createdComment.updatedAt type:', typeof createdComment.updatedAt);
  notesLogger.debug('[DEBUG] createdComment.createdAt value:', createdComment.createdAt);
  notesLogger.debug('[DEBUG] createdComment.updatedAt value:', createdComment.updatedAt);

  await createCommentHistory(input.noteId, sanitizedContent, input.authorId);

  const note = await getNoteById(input.noteId);
  notesLogger.debug('[DEBUG] Retrieved note for event:', note);
  
  // FIXED: Always use new Date().toISOString() to avoid type issues completely
  const dateISO = new Date().toISOString();
  notesLogger.debug('[DEBUG] Using guaranteed ISO string:', dateISO);

  notesLogger.debug('[DEBUG] Prepared event data:', {
    noteId: input.noteId,
    commentId,
    authorId: input.authorId,
    noteTitle: note.title,
    isPublic: note.isPublic,
    content: sanitizedContent,
    lastCommentAt: dateISO,
    lastCommentAuthorId: input.authorId,
  });

  await emitCommentCreatedEvent({
    noteId: input.noteId,
    commentId,
    authorId: input.authorId,
    noteTitle: note.title,
    isPublic: note.isPublic,
    content: sanitizedContent,
    lastCommentAt: dateISO,
    lastCommentAuthorId: input.authorId,
  });

  notesLogger.info(`Comment created for note ${input.noteId}`);
  
  // FIXED: Return comment with guaranteed ISO string dates for frontend reactivity
  const responseComment = {
    ...createdComment,
    createdAt: dateISO,
    updatedAt: dateISO,
  };
  
  notesLogger.debug('[DEBUG] Returning comment from service with guaranteed ISO dates:', responseComment);
  return responseComment;
}

// Get history for a note
export async function getNoteHistory(noteId: string) {
  return await findHistory(noteId);
}

/**
 * Mark note as viewed by updating lastViewedAt in note_layouts
 * This is the unified method for tracking when a user views a note
 */
export async function markNoteAsViewed(noteId: string, userId: string) {
  notesLogger.debug(`[markNoteAsViewed] Marking note ${noteId} as viewed by user ${userId}`);
  
  // Update or insert the layout record with lastViewedAt
  await db
    .insert(noteLayouts)
    .values({
      id: crypto.randomUUID(),
      noteId,
      userId,
      lastViewedAt: new Date(),
      // Preserve existing values if they exist
      column: 1,
      positionY: 0,
      height: 220,
    })
    .onDuplicateKeyUpdate({
      set: {
        lastViewedAt: new Date(),
        updatedAt: new Date(),
      },
    });
  
  notesLogger.debug(`[markNoteAsViewed] Note ${noteId} marked as viewed by user ${userId}`);
}

// Export notes to CSV
export { exportNotesToCSV };
