// src/server/features/personnel/personnel.service.ts
// Business logic for personnel management (unified employees table)

import { db } from '@serverShared/db/client';
import { employees } from './db/employees.table';
import { employmentPeriods } from './db/employment_periods.table';
import { rolePermissionsTable } from '../system/db/role_permissions.table';
import { eq, and, or, gt, lte, ne, isNull } from 'drizzle-orm';
import { hash } from 'argon2';
import { randomUUID } from 'node:crypto';
import { dayjs } from '@serverShared/lib/dayjs';
import { createExtendedBaseService } from '@serverShared/db/base.service';
import { AppError } from '@serverShared/lib/errors';
import {
  emitEmployeeCreatedEvent,
  emitEmployeeUpdatedEvent,
} from './personnel.events';
import {
  getMaidPeriodsForUser,
  getMaidPeriodsForUsers,
  hasActiveMaidPeriod,
  validateMaidPeriods,
  syncMaidPeriods,
  findLatestManagerPeriod,
  createManagerPeriod,
  updateManagerPeriod,
  getUsersForScheduleGrid,
} from './lib/employment.service';
import { fireEmployee, rehireEmployee, restoreEmployee } from './lib/lifecycle.service';
// Shared contracts
import type {
  EmployeeInput,
  EmployeeUpdate,
  RehireEmployeeInput,
  MaidPeriod,
  Employee,
} from '@shared/contracts/personnel';
import { AppPermission } from '@shared/contracts/permissions';
import { USER_ROLES } from '@shared/constants/roles';
import { getRolesWithPermission } from '@serverShared/lib/rbac';

// Standard password hash for MAID role
const MAID_DEFAULT_PASSWORD_HASH = '$argon2id$v=19$m=65536,t=3,p=4$mKerV96B8kLqwMsOcj+ODA$Qcj4acTHAIC6f6QQqoYXWmHA624u76f6+2QXKyr3UCQ';

// Create base service for employees
const employeesBaseService = createExtendedBaseService(employees);

async function checkRoleHasManagerTrack(tx: any, role: string | null): Promise<boolean> {
  if (!role) return false;

  const perms = await tx
    .select()
    .from(rolePermissionsTable)
    .where(eq(rolePermissionsTable.role, role));

  return perms.some((p: any) => (
    p.isEnabled === true
    && `${p.resource}:${p.action}` === AppPermission.SCHEDULE_TRACK_MANAGER
  ));
}

/**
 * Маппинг строки БД в Employee Response с явным приведением типов
 */
export function mapToEmployeeResponse(
  row: any,
  maidPeriods: MaidPeriod[],
  isMentionable?: boolean,
): Employee {
  return {
    id: row.id,
    fullName: row.fullName,
    position: row.position,
    phone: row.phone,
    email: row.email,
    birthDate: row.birthDate ? row.birthDate.toISOString() : null,
    address: row.address,
    notes: row.notes,
    isFired: Boolean(row.isFired),
    firedReason: row.firedReason,
    username: row.username,
    passwordHash: row.passwordHash,
    role: row.role,
    isMentionable,
    // GOD role never has maid status
    isMaidAlso: row.role === USER_ROLES.GOD ? false : hasActiveMaidPeriod(maidPeriods),
    maidPeriods,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    archivedAt: row.archivedAt ? row.archivedAt.toISOString() : null,
  };
}

// Generate 4-digit unique employee ID
export async function generateEmployeeId(): Promise<string> {
  return randomUUID();
}

// Get all users (non-fired)
export async function getEmployees(currentUserId: string): Promise<Employee[]> {
  const mentionableRoles = await getRolesWithPermission(AppPermission.SYSTEM_RECEIVE_MENTIONS);

  const result = await db
    .select()
    .from(employees)
    .where(
      and(
        eq(employees.isSystem, false),
        eq(employees.isFired, false),
        or(
          isNull(employees.role),
          ne(employees.role, USER_ROLES.GOD),
          eq(employees.id, currentUserId)
        )
      )
    );
  
  const userIds = result.map(emp => emp.id);
  const maidPeriodsMap = await getMaidPeriodsForUsers(userIds);
  
  return result.map(emp => {
    const empMaidPeriods = maidPeriodsMap.get(emp.id) || [];
    const isMentionable = (emp.role !== null && mentionableRoles.includes(emp.role)) || emp.role === USER_ROLES.GOD;
    return mapToEmployeeResponse(emp, empMaidPeriods, isMentionable);
  });
}

// Get users by status (active or fired) with effective status logic
export async function getEmployeesByStatus(isFired: boolean, currentUserId: string): Promise<Employee[]> {
  const mskNow = dayjs().tz('Europe/Moscow').toDate();
  const mentionableRoles = await getRolesWithPermission(AppPermission.SYSTEM_RECEIVE_MENTIONS);

  let whereCondition;
  if (isFired === false) {
    whereCondition = and(
      eq(employees.isSystem, false),
      or(
        eq(employees.isFired, false),
        and(
          eq(employees.isFired, true),
          gt(employees.archivedAt, mskNow)
        )
      ),
      or(
        isNull(employees.role),
        ne(employees.role, USER_ROLES.GOD),
        eq(employees.id, currentUserId)
      )
    );
  } else {
    whereCondition = and(
      eq(employees.isSystem, false),
      eq(employees.isFired, true),
      lte(employees.archivedAt, mskNow),
      or(
        isNull(employees.role),
        ne(employees.role, USER_ROLES.GOD),
        eq(employees.id, currentUserId)
      )
    );
  }
  
  const result = await db
    .select()
    .from(employees)
    .where(whereCondition);
  
  const userIds = result.map(emp => emp.id);
  const maidPeriodsMap = await getMaidPeriodsForUsers(userIds);
  
  return result.map(emp => {
    const empMaidPeriods = maidPeriodsMap.get(emp.id) || [];
    const isMentionable = (emp.role !== null && mentionableRoles.includes(emp.role)) || emp.role === USER_ROLES.GOD;
    return mapToEmployeeResponse(emp, empMaidPeriods, isMentionable);
  });
}

// Get employee by ID
export async function getEmployeeById(id: string): Promise<Employee | null> {
  const result = await db
    .select()
    .from(employees)
    .where(and(eq(employees.id, id), eq(employees.isSystem, false)))
    .limit(1);
  
  if (result.length === 0) {
    return null;
  }
  
  const emp = result[0];
  const empMaidPeriods = await getMaidPeriodsForUser(id);
  const mentionableRoles = await getRolesWithPermission(AppPermission.SYSTEM_RECEIVE_MENTIONS);
  const isMentionable = (emp.role !== null && mentionableRoles.includes(emp.role)) || emp.role === USER_ROLES.GOD;
  
  return mapToEmployeeResponse(emp, empMaidPeriods, isMentionable);
}

// Get employee by ID (including archived)
export async function getEmployeeByIdIncludingArchived(id: string): Promise<Employee | null> {
  const result = await db
    .select()
    .from(employees)
    .where(and(eq(employees.id, id), eq(employees.isSystem, false)))
    .limit(1);
  
  if (result.length === 0) {
    return null;
  }
  
  const emp = result[0];
  const empMaidPeriods = await getMaidPeriodsForUser(id);
  const mentionableRoles = await getRolesWithPermission(AppPermission.SYSTEM_RECEIVE_MENTIONS);
  const isMentionable = (emp.role !== null && mentionableRoles.includes(emp.role)) || emp.role === USER_ROLES.GOD;
  
  return mapToEmployeeResponse(emp, empMaidPeriods, isMentionable);
}

// Create employee
export async function createEmployee(data: EmployeeInput): Promise<string> {
  const employeeId = await generateEmployeeId();
  const now = new Date();
  const hireDate = data.hireDate ? new Date(data.hireDate) : now;
  const birthDateValue = data.birthDate ? new Date(data.birthDate) : null;

  let passwordHash: string | null = null;
  if (data.password) {
    passwordHash = await hash(data.password);
  } else if (data.role === USER_ROLES.MAID) {
    // Use default password hash for MAID role
    passwordHash = MAID_DEFAULT_PASSWORD_HASH;
  }

  // Username is now required for all roles including MAID
  const username = data.username;

  if (data.maidPeriods && data.maidPeriods.length > 0) {
    validateMaidPeriods(data.maidPeriods);
  }

  await db.transaction(async (tx) => {
    await tx
      .insert(employees)
      .values({
        id: employeeId,
        fullName: data.fullName,
        position: data.position,
        phone: data.phone,
        email: data.email || null,
        birthDate: birthDateValue,
        address: data.address || null,
        notes: data.notes || null,
        username: username || null,
        passwordHash,
        role: (data.role as 'ADMIN' | 'MANAGER' | 'MAID' | 'VIEWER' | 'GOD' | null) || null,
        isFired: false,
        firedReason: null,
        createdAt: hireDate,
        updatedAt: now,
      });

    if (data.username) {
      const hasManagerTrack = await checkRoleHasManagerTrack(tx, data.role || null);

      if (hasManagerTrack) {
        const startDateOnly = dayjs(hireDate).format('YYYY-MM-DD');
        await tx
          .insert(employmentPeriods)
          .values({
            id: randomUUID(),
            userId: employeeId,
            startDate: startDateOnly as any,
            endDate: null,
            isMaid: false,
            createdAt: now,
          });
      }

      if (data.maidPeriods && data.maidPeriods.length > 0) {
        for (const period of data.maidPeriods) {
          const startDate = dayjs(period.startDate).format('YYYY-MM-DD');
          const endDate = period.endDate ? dayjs(period.endDate).format('YYYY-MM-DD') : null;
          
          await tx
            .insert(employmentPeriods)
            .values({
              id: period.id || randomUUID(),
              userId: employeeId,
              startDate: startDate as any,
              endDate: endDate as any,
              isMaid: true,
              createdAt: now,
            });
        }
      }
    }
  });

  const employee = await getEmployeeById(employeeId);
  if (employee) {
    await emitEmployeeCreatedEvent({
      employeeId,
      employee,
    });
  }

  return employeeId;
}

// Update employee
export async function updateEmployee(id: string, data: EmployeeUpdate): Promise<void> {
  const employee = await getEmployeeById(id);
  if (!employee) {
    throw new Error('Employee not found');
  }

  const isPromotingToUser = data.createUser === true && !employee.username;
  const targetRole = data.role || employee.role || USER_ROLES.MANAGER;

  if (isPromotingToUser && !data.username) {
    throw new AppError('Логин обязателен для создания учетной записи', 400, 'VALIDATION_ERROR');
  }

  if (isPromotingToUser && targetRole !== USER_ROLES.MAID && !data.password) {
    throw new AppError('Пароль обязателен для создания учетной записи', 400, 'VALIDATION_ERROR');
  }

  // Защита GOD-аккаунта от смены роли
  if (employee.role === USER_ROLES.GOD && data.role !== undefined && data.role !== USER_ROLES.GOD) {
    throw new AppError('Нельзя изменить роль Создателя', 403, 'FORBIDDEN');
  }

  const birthDateValue = data.birthDate ? new Date(data.birthDate) : null;
  const hireDateValue = data.hireDate ? new Date(data.hireDate) : undefined;

  let passwordHash: string | null = null;
  if (data.password) {
    passwordHash = await hash(data.password);
  } else if ((targetRole === USER_ROLES.MAID || data.role === USER_ROLES.MAID) && (!employee.passwordHash || isPromotingToUser)) {
    // Use default password hash for MAID role if no password hash exists
    // or when account is created for MAID from edit mode
    passwordHash = MAID_DEFAULT_PASSWORD_HASH;
  }

  if (data.maidPeriods && data.maidPeriods.length > 0) {
    validateMaidPeriods(data.maidPeriods);
  }

  const usernameForUpdate = isPromotingToUser
    ? data.username
    : (targetRole !== USER_ROLES.MAID ? data.username : undefined);

  const updateData: any = {
    fullName: data.fullName,
    position: data.position,
    phone: data.phone,
    email: data.email,
    birthDate: birthDateValue,
    address: data.address || null,
    notes: data.notes || null,
    // При создании учетной записи в edit-режиме обновляем логин для любой роли,
    // в остальных случаях сохраняем прежнее поведение (для MAID логин не трогаем).
    username: usernameForUpdate,
    role: data.role !== undefined ? (data.role as 'ADMIN' | 'MANAGER' | 'MAID' | 'VIEWER' | 'GOD' | null) : undefined,
    updatedAt: new Date(),
  };

  if (passwordHash !== null) {
    updateData.passwordHash = passwordHash;
  }

  if (hireDateValue !== undefined) {
    updateData.createdAt = hireDateValue;
  }

  if (data.firedReason !== undefined) {
    updateData.firedReason = data.firedReason;
  }
  if (data.isFired !== undefined) {
    updateData.isFired = data.isFired;
  }

  if (data.archivedAt !== undefined) {
    const archivedAtDate = dayjs.utc(data.archivedAt).tz('Europe/Moscow').startOf('day').utc().toDate();
    updateData.archivedAt = archivedAtDate;
  }

  await db.transaction(async (tx) => {
    await tx
      .update(employees)
      .set(updateData)
      .where(eq(employees.id, id));

    // PBAC: Интеллектуальное управление периодами менеджера при смене роли
    if (data.role !== undefined && data.role !== employee.role) {
      const oldRoleHasTrack = await checkRoleHasManagerTrack(tx, employee.role);
      const newRoleHasTrack = await checkRoleHasManagerTrack(tx, targetRole);
      const todayOnly = dayjs().tz('Europe/Moscow').format('YYYY-MM-DD');

      if (!newRoleHasTrack && oldRoleHasTrack) {
        // Роль лишилась права на сетку администраторов: нужно закрыть период
        const latestManagerPeriod = await findLatestManagerPeriod(id, tx);
        if (latestManagerPeriod && !latestManagerPeriod.endDate) {
          if (latestManagerPeriod.startDate === todayOnly) {
            // Если период открыт сегодня (ошибка при создании) - физически удаляем
            await tx
              .delete(employmentPeriods)
              .where(eq(employmentPeriods.id, latestManagerPeriod.id));
          } else {
            // Иначе закрываем период сегодняшним днем, чтобы сохранить историю прошлых смен
            await tx
              .update(employmentPeriods)
              .set({ endDate: todayOnly as any })
              .where(eq(employmentPeriods.id, latestManagerPeriod.id));
          }
        }
      } else if (newRoleHasTrack && !oldRoleHasTrack) {
        // Роль получила право на сетку администраторов: нужно открыть период
        const latestManagerPeriod = await findLatestManagerPeriod(id, tx);
        if (!latestManagerPeriod || latestManagerPeriod.endDate) {
          await createManagerPeriod(id, todayOnly, tx);
        }
      }
    }

    if (data.archivedAt !== undefined && employee.isFired) {
      const archivedAtDate = dayjs.utc(data.archivedAt).tz('Europe/Moscow').startOf('day').utc().toDate();
      const endDateOnly = dayjs.utc(archivedAtDate).tz('Europe/Moscow').format('YYYY-MM-DD');

      const latestManagerPeriod = await findLatestManagerPeriod(id, tx);

      if (latestManagerPeriod) {
        await tx
          .update(employmentPeriods)
          .set({
            endDate: endDateOnly as any,
          })
          .where(eq(employmentPeriods.id, latestManagerPeriod.id));
      }
    }

    if (hireDateValue !== undefined) {
      const startDateOnly = dayjs(hireDateValue).format('YYYY-MM-DD');
      const existingManagerPeriod = await findLatestManagerPeriod(id, tx);

      if (existingManagerPeriod) {
        await updateManagerPeriod(existingManagerPeriod.id, startDateOnly, tx);
      } else {
        const hasManagerTrack = await checkRoleHasManagerTrack(tx, targetRole);
        if (hasManagerTrack) {
          await createManagerPeriod(id, startDateOnly, tx);
        }
      }
    }

    if (data.maidPeriods !== undefined) {
      await syncMaidPeriods(id, data.maidPeriods, tx);
    }
  });

  const updatedEmployee = await getEmployeeById(id);
  if (updatedEmployee) {
    await emitEmployeeUpdatedEvent({
      employeeId: id,
      employee: updatedEmployee,
    });
  }
}

// Delete employee (hard delete) — allowed only if hire date (createdAt) is in the future
export async function deleteEmployee(id: string): Promise<void> {
  if (!id) {
    throw new Error("CRITICAL: Attempted to delete employee without ID");
  }

  const mskNow = dayjs().tz('Europe/Moscow').startOf('day');
  const rows = await db
    .select({ createdAt: employees.createdAt })
    .from(employees)
    .where(eq(employees.id, id))
    .limit(1);

  if (rows.length === 0) {
    throw new AppError('Сотрудник не найден', 404, 'NOT_FOUND');
  }

  const hireDate = dayjs(rows[0].createdAt).tz('Europe/Moscow').startOf('day');
  if (!hireDate.isAfter(mskNow)) {
    throw new AppError(
      'Удаление допускается только для сотрудников с будущей датой приёма',
      400,
      'VALIDATION_ERROR',
    );
  }

  await db.transaction(async (tx) => {
    await tx
      .delete(employmentPeriods)
      .where(eq(employmentPeriods.userId, id));
    await tx
      .delete(employees)
      .where(eq(employees.id, id));
  });
}

// Check username availability
export async function checkUsernameAvailability(username: string): Promise<boolean> {
  const existingEmployee = await db
    .select()
    .from(employees)
    .where(eq(employees.username, username))
    .limit(1);

  return existingEmployee.length === 0;
}

// Export users to CSV with BOM for Cyrillic support
export function exportEmployeesToCSV(employeeList: Employee[]): string {
  const BOM = '\uFEFF';
  const header = 'ID,ФИО,Должность,Телефон,Email,Дата рождения,Адрес,Заметки,Уволен,Причина увольнения,Дата создания';

  const rows = employeeList.map(emp => {
    const birthDate = emp.birthDate ? new Date(emp.birthDate).toISOString().split('T')[0] : '';
    const createdAt = new Date(emp.createdAt).toISOString().split('T')[0];
    const isFired = emp.isFired ? 'Да' : 'Нет';

    const escapeField = (field: string | null) => {
      if (field === null || field === '') return '';
      if (field.includes(',') || field.includes('"') || field.includes('\n')) {
        return `"${field.replace(/"/g, '""')}"`;
      }
      return field;
    };

    return [
      emp.id,
      escapeField(emp.fullName),
      escapeField(emp.position),
      escapeField(emp.phone),
      escapeField(emp.email),
      birthDate,
      escapeField(emp.address || ''),
      escapeField(emp.notes || ''),
      isFired,
      escapeField(emp.firedReason || ''),
      createdAt,
    ].join(',');
  });

  return BOM + header + '\n' + rows.join('\n');
}

// Re-export lifecycle functions
export { fireEmployee, rehireEmployee, restoreEmployee, getUsersForScheduleGrid };

// Re-export Employee type for other services
export type { Employee };
