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

import crypto from 'node:crypto';
import { db } from '@serverShared/db/client';
import { notes } from './db/notes.table';
import { noteComments } from './db/note_comments.table';
import { noteLayouts } from './db/note_layouts.table';
import { users } from '@features/personnel/db/employees.table';
import { rolePermissionsTable } from '@features/system/db/role_permissions.table';
import { loggerService } from '@serverShared/lib/logger';
import { createExtendedBaseService } from '@serverShared/db/base.service';
import { sanitizeHtml } from '@serverShared/lib/sanitize';
import type { UserInfo } from '@serverShared/lib/auth';
import { hasPermission } from '@serverShared/plugins/rbac';
import { AppPermission } from '@shared/contracts/permissions';
import { eq, desc, and, like, inArray } from 'drizzle-orm';
import { EVENTS } from '@shared/contracts/events';
import { dayjs } from '@serverShared/lib/dayjs';
import { USER_ROLES } from '@shared/constants/roles';
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,
  initializeLayoutsForPublicNote,
  initializeLayoutsForPrivateNote,
  deleteLayoutsForNote,
} from './lib/layout.service';
import {
  handleContentUpdate,
} 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');

async function getAuthorityRoles(tx?: any): Promise<string[]> {
  const database = tx || db;
  const perms = await database
    .select({ role: rolePermissionsTable.role })
    .from(rolePermissionsTable)
    .where(
      and(
        eq(rolePermissionsTable.resource, 'notes'),
        eq(rolePermissionsTable.action, 'authority_highlight'),
        eq(rolePermissionsTable.isEnabled, true)
      )
    );

  const roles = perms.map((p: any) => p.role);
  if (!roles.includes(USER_ROLES.GOD)) roles.push(USER_ROLES.GOD);
  return roles;
}

async function getAuthorRolesMap(authorIds: string[]): Promise<Map<string, string>> {
  if (authorIds.length === 0) return new Map();

  const rows = await db
    .select({ id: users.id, role: users.role })
    .from(users)
    .where(inArray(users.id, authorIds));

  return new Map(rows.map((row) => [row.id, row.role ?? '']));
}

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);
  const authorityRoles = await getAuthorityRoles();
  const authorIds = Array.from(new Set(result.map((note: any) => note.authorId).filter(Boolean)));
  const authorRolesMap = await getAuthorRolesMap(authorIds);

  const notesWithAuthority = result.map((note: any) => {
    const authorRole = authorRolesMap.get(note.authorId);
    const isAuthority = authorRole ? authorityRoles.includes(authorRole) : false;
    return {
      ...note,
      isAuthority,
    };
  });

  notesLogger.info(`getNotes: userId=${userId}, totalNotes=${notesWithAuthority.length}`);
  return notesWithAuthority;
}

// 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');

  const authorityRoles = await getAuthorityRoles();
  const [author] = await db
    .select({ role: users.role })
    .from(users)
    .where(eq(users.id, (result as any).authorId))
    .limit(1);

  const isAuthority = author?.role ? authorityRoles.includes(author.role) : false;

  return {
    ...(result as any),
    isAuthority,
  };
}

// 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');

  const authorityRoles = await getAuthorityRoles();
  const [author] = await db
    .select({ role: users.role })
    .from(users)
    .where(eq(users.id, result.authorId))
    .limit(1);

  const isAuthority = author?.role ? authorityRoles.includes(author.role) : false;

  return {
    ...result,
    isAuthority,
  };
}

// 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');

    // Initialize layouts for note visibility (strict PBAC)
    if (input.isPublic === true) {
      notesLogger.debug('[DEBUG] Starting distribution...');
      try {
        await initializeLayoutsForPublicNote(
          noteId,
          input.authorId,
          input.height ?? 220
        );
      } 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;

  if (isPublicChanged && user) {
    if (input.isPublic === true) {
      const canCreatePublic = await hasPermission(user, AppPermission.NOTES_CREATE_PUBLIC);
      if (!canCreatePublic) throw new Error('У вас нет прав делать заметки публичными');
    } else if (input.isPublic === false) {
      const canCreatePrivate = await hasPermission(user, AppPermission.NOTES_CREATE_PRIVATE);
      if (!canCreatePrivate) throw new Error('У вас нет прав скрывать заметки в личные');
    }
  }

  // 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('Необходимо указать причину изменения статуса');
    }
  }

  const canEdit = user ? await hasPermission(user, AppPermission.NOTES_EDIT) : false;
  const canEditAfterComment = user ? await hasPermission(user, AppPermission.NOTES_EDIT_AFTER_COMMENT) : false;

  // Validation for public notes (Strict PBAC)
  if (existingNote.isPublic && user) {
    // 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('Нельзя скрыть заметку, в которой уже есть комментарии');
        }
      }

      // 2. Проверка редактирования контента (title или content)
      if (isChangingContent) {
        if (hasComments && !canEditAfterComment) {
          throw new Error('Редактирование запрещено: в заметке есть комментарии');
        }
        if (!canEdit && !canEditAfterComment) {
          throw new Error('У вас нет прав на редактирование заметок');
        }

        // НОВАЯ ПРОВЕРКА: Если не автор, требуем право EDIT_ANY
        const canEditAny = await hasPermission(user, AppPermission.NOTES_EDIT_ANY as any);
        if (!isAuthor && !canEditAny) {
          throw new Error('У вас нет прав на редактирование текста чужих заметок');
        }
      }
    }
  }

  // Проверяем изменение приоритета и устанавливаем 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);

  const updatedNote = await getNoteById(id);
  const statusChanged = input.status !== undefined && input.status !== existingNote.status;

  if (statusChanged) {
    if (input.status === 'done' || input.status === 'cancelled') {
      await deleteLayoutsForNote(id);
      notesLogger.info(`[LAYOUT] Note ${id} layouts deleted (status=${input.status})`);
    }

    if (
      input.status === 'active'
      && (existingNote.status === 'done' || existingNote.status === 'cancelled')
    ) {
      if (existingNote.isPublic) {
        await initializeLayoutsForPublicNote(id, existingNote.authorId, input.height ?? 220);
      } else {
        await initializeLayoutsForPrivateNote(id, existingNote.authorId, input.height ?? 220);
      }
      notesLogger.info(`[LAYOUT] Note ${id} layouts reinitialized on active restore`);
    }
  }

  if (isPublicChanged) {
    await deleteLayoutsForNote(id);

    if (updatedNote.status === 'active') {
      if (input.isPublic === true) {
        await initializeLayoutsForPublicNote(id, existingNote.authorId, input.height ?? 220);
      }

      if (input.isPublic === false) {
        await initializeLayoutsForPrivateNote(id, existingNote.authorId, input.height ?? 220);
      }
    }
  }

  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);

  const canDeleteAny = await hasPermission(user, AppPermission.NOTES_DELETE_ANY);
  const canDeleteOwn = await hasPermission(user, AppPermission.NOTES_DELETE_OWN);
  const isAuthor = user.id === existingNote.authorId;

  if (canDeleteAny) {
    await hardDeleteNote(id);
    await deleteAllNotificationsByNoteId(id);
    await emitNoteDeletedEvent({ noteId: id });
    notesLogger.info(`Note hard deleted by user with DELETE_ANY permission: ${id}`);
    return;
  }

  if (canDeleteOwn && isAuthor) {
    const comments = await findComments(id);
    if (comments.length > 0) {
      throw new Error('Нельзя удалить заметку, в которой есть обсуждение');
    }

    await hardDeleteNote(id);
    await deleteAllNotificationsByNoteId(id);
    await emitNoteDeletedEvent({ noteId: id });
    notesLogger.info(`Note hard deleted by author: ${id}`);
    return;
  }

  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 };
