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

import { db } from '@serverShared/db/client';
import { rolePermissionsTable, roleColorsTable, systemModulesTable, roleSessionPoliciesTable } from './db';
import { eq, and } from 'drizzle-orm';
import { loggerService } from '@serverShared/lib/logger';
import type { LogChannel, LogLevel, LogConfig } from '@shared/contracts/logger';
import { AppPermission } from '@shared/contracts/permissions';
import { logger } from '@serverShared/lib/logger';
import { emitEvent } from '@serverShared/lib/events';
import { USER_ROLES } from '@shared/constants/roles';

// Required modules for Smart Seeding
const REQUIRED_MODULES = [
  'inventory', 'tasks', 'notes', 'personnel', 'schedule',
  'paymaster', 'rooms', 'blacklist', 'contractors',
  'access', 'equipment', 'operations'
];

// Types
export interface RolePermission {
  id: string;
  role: string;
  resource: string;
  action: string;
  isEnabled: boolean;
  createdAt: Date;
  updatedAt: Date;
}

export interface SystemModule {
  id: string;
  moduleId: string;
  isEnabled: boolean;
  createdAt: Date;
  updatedAt: Date;
}

export interface RoleColors {
  id: string;
  role: string;
  privateNormal: string;
  privateHigh: string;
  publicNormal: string;
  publicHigh: string;
  createdAt: Date;
  updatedAt: Date;
}

export interface UpsertRoleColorsInput {
  privateNormal: string;
  privateHigh: string;
  publicNormal: string;
  publicHigh: string;
}

export interface CreateRolePermissionInput {
  role: string;
  resource: string;
  action: string;
  isEnabled?: boolean;
}

export interface UpdateRolePermissionInput {
  id: string;
  isEnabled: boolean;
}

export interface UpsertRolePermissionInput {
  role: string;
  resource: string;
  action: string;
  isEnabled: boolean;
}

export interface CreateSystemModuleInput {
  moduleId: string;
  isEnabled?: boolean;
}

export interface UpdateSystemModuleInput {
  id: string;
  isEnabled: boolean;
}

const PAYMASTER_RADIO_ACTIONS = ['read_only', 'today_only', 'yesterday_too', 'any_date'];
const PAYMASTER_RADIO_RANK = new Map(PAYMASTER_RADIO_ACTIONS.map((a, i) => [a, i]));

async function migratePaymasterRadioConflicts(now: Date): Promise<void> {
  const allPerms = await db.select().from(rolePermissionsTable);
  const radioPerms = allPerms.filter(
    p => p.resource === 'paymaster' && PAYMASTER_RADIO_ACTIONS.includes(p.action) && p.isEnabled
  );
  if (radioPerms.length === 0) return;

  const byRole = new Map<string, typeof radioPerms>();
  for (const p of radioPerms) {
    if (!byRole.has(p.role)) byRole.set(p.role, []);
    byRole.get(p.role)!.push(p);
  }
  for (const [role, perms] of byRole) {
    if (perms.length <= 1) continue;
    perms.sort((a, b) => (PAYMASTER_RADIO_RANK.get(b.action) ?? 0) - (PAYMASTER_RADIO_RANK.get(a.action) ?? 0));
    for (let i = 1; i < perms.length; i++) {
      await db
        .update(rolePermissionsTable)
        .set({ isEnabled: false, updatedAt: now })
        .where(eq(rolePermissionsTable.id, perms[i].id));
      logger.info(`[System Service] Paymaster radio migration: disabled ${role}:${perms[i].action}`);
    }
  }
}

function getDefaultPermissionState(role: string, permission: AppPermission, managerEnabledPermissions: Set<string>): boolean {
  const [resource, action] = permission.split(':');

  if (!resource || !action) {
    return false;
  }

  if (resource === 'paymaster' && PAYMASTER_RADIO_ACTIONS.includes(action)) {
    if (role === 'GOD' || role === 'ADMIN') {
      return action === 'any_date';
    }
    if (role === 'MANAGER') {
      return action === 'today_only';
    }
    return false;
  }

  if (role === 'GOD') {
    return true;
  }

  if (role === 'ADMIN') {
    return !(resource === 'personnel' && (action === 'manage' || action === 'delete'));
  }

  if (role === 'MAID') {
    return permission === AppPermission.NOTES_READ
      || permission === AppPermission.TASKS_ACCESS
      || permission === AppPermission.CHECKIN_READ;
  }

  if (role === 'VIEWER') {
    return action === 'read' && resource !== 'paymaster' && resource !== 'cleaning';
  }

  if (role === 'MANAGER') {
    return managerEnabledPermissions.has(permission);
  }

  return false;
}

/**
 * Get current log configuration
 * @returns Current log configuration
 */
export function getLogConfig(): LogConfig {
  return loggerService.getConfig();
}

/**
 * Update log level for a specific channel
 * @param channel - The channel to update
 * @param level - The new log level
 * @returns Updated log configuration
 */
export function updateLogConfig(channel: LogChannel, level: LogLevel): LogConfig {
  loggerService.updateConfig(channel, level);
  return loggerService.getConfig();
}

/**
 * Reset all log levels to default
 * @returns Default log configuration
 */
export function resetLogConfig(): LogConfig {
  loggerService.resetConfig();
  return loggerService.getConfig();
}

/**
 * Get all role permissions (with Smart Seeding)
 * @returns List of all role permissions
 */
export async function getAllRolePermissions(): Promise<RolePermission[]> {
  const permissions = await db.select().from(rolePermissionsTable);

  const rolesToSeed = ['GOD', 'ADMIN', 'MANAGER', 'MAID', 'VIEWER'];
  const managerEnabledPermissions = new Set<string>([
    AppPermission.NOTES_READ,
    AppPermission.NOTES_CREATE_PRIVATE,
    AppPermission.NOTES_CREATE_PUBLIC,
    AppPermission.NOTES_EDIT,
    AppPermission.NOTES_CHANGE_PRIORITY,
    AppPermission.NOTES_CHANGE_STATUS,
    AppPermission.NOTES_DELETE_OWN,
    AppPermission.TASKS_ACCESS,
    AppPermission.PERSONNEL_READ,
    AppPermission.SCHEDULE_READ,
    AppPermission.SCHEDULE_REQUEST_APPROVAL,
    AppPermission.BLACKLIST_READ,
    AppPermission.BLACKLIST_REQUEST_APPROVAL,
    AppPermission.ACCESS_REQUEST_APPROVAL,
    AppPermission.CONTRACTORS_READ,
    AppPermission.CONTRACTORS_REQUEST_APPROVAL,
    AppPermission.ROOMS_READ,
    AppPermission.INVENTORY_READ,
    AppPermission.INVENTORY_REQUEST_APPROVAL,
    AppPermission.CHECKIN_READ,
    AppPermission.CHECKIN_WRITE,
    AppPermission.CLEANING_MANAGE,
    AppPermission.CLEANING_READ,
    AppPermission.PAYMASTER_TODAY_ONLY,
    AppPermission.SYSTEM_RECEIVE_MENTIONS,
    AppPermission.PERSONNEL_ASSIGN_MAID,

  ]);

  const now = new Date();
  const existingKeys = new Set(
    permissions.map((item) => `${item.role}:${item.resource}:${item.action}`)
  );

  // One-time migration: rename resource 'operations' → 'checkin'
  const hasLegacyOperations = permissions.some(p => p.resource === 'operations');
  if (hasLegacyOperations) {
    logger.info('[System Service] Migrating legacy resource "operations" → "checkin"...');
    await db
      .update(rolePermissionsTable)
      .set({ resource: 'checkin', updatedAt: now })
      .where(eq(rolePermissionsTable.resource, 'operations'));
    const migrated = await db.select().from(rolePermissionsTable);
    existingKeys.clear();
    migrated.forEach((item) => existingKeys.add(`${item.role}:${item.resource}:${item.action}`));
    logger.info('[System Service] Migration complete: operations → checkin');
  }

  await migratePaymasterRadioConflicts(now);

  if (permissions.length === 0) {
    logger.info('[System Service] Permissions matrix is empty. Running Smart Seeder...');
  }

  for (const role of rolesToSeed) {
    for (const permission of Object.values(AppPermission)) {
      const [resource, action] = permission.split(':');
      if (!resource || !action) continue;

      const key = `${role}:${resource}:${action}`;
      if (existingKeys.has(key)) {
        continue;
      }

      await db.insert(rolePermissionsTable).values({
        id: crypto.randomUUID(),
        role,
        resource,
        action,
        isEnabled: getDefaultPermissionState(role, permission, managerEnabledPermissions),
        createdAt: now,
        updatedAt: now,
      });

      existingKeys.add(key);
    }
  }

  const normalizedPermissions = await db.select().from(rolePermissionsTable);

  return normalizedPermissions.map(p => ({
    id: p.id,
    role: p.role,
    resource: p.resource,
    action: p.action,
    isEnabled: p.isEnabled,
    createdAt: p.createdAt,
    updatedAt: p.updatedAt,
  }));
}

/**
 * Get permissions for a specific role
 * @param role - The role to get permissions for
 * @returns List of permissions for the role
 */
export async function getRolePermissions(role: string): Promise<RolePermission[]> {
  await migratePaymasterRadioConflicts(new Date());

  let permissions = await db
    .select()
    .from(rolePermissionsTable)
    .where(eq(rolePermissionsTable.role, role));

  if (permissions.length === 0) {
    const all = await getAllRolePermissions();
    permissions = all.filter(p => p.role === role);
  }

  return permissions.map(p => ({
    id: p.id,
    role: p.role,
    resource: p.resource,
    action: p.action,
    isEnabled: p.isEnabled,
    createdAt: p.createdAt,
    updatedAt: p.updatedAt,
  }));
}

/**
 * Create a new role permission
 * @param input - Permission data
 * @returns Created permission
 */
export async function createRolePermission(input: CreateRolePermissionInput): Promise<RolePermission> {
  const now = new Date();
  const id = crypto.randomUUID();
  
  await db
    .insert(rolePermissionsTable)
    .values({
      id,
      role: input.role,
      resource: input.resource,
      action: input.action,
      isEnabled: input.isEnabled ?? true,
      createdAt: now,
      updatedAt: now,
    });
    
  const [permission] = await db
    .select()
    .from(rolePermissionsTable)
    .where(eq(rolePermissionsTable.id, id))
    .limit(1);
    
  logger.info(`Created role permission: ${input.role}:${input.resource}:${input.action}`);
  
  return {
    id: permission.id,
    role: permission.role,
    resource: permission.resource,
    action: permission.action,
    isEnabled: permission.isEnabled,
    createdAt: permission.createdAt,
    updatedAt: permission.updatedAt,
  };
}

/**
 * Update a role permission
 * @param input - Permission update data
 * @returns Updated permission
 */
export async function updateRolePermission(input: UpdateRolePermissionInput): Promise<RolePermission> {
  const now = new Date();
  
  await db
    .update(rolePermissionsTable)
    .set({
      isEnabled: input.isEnabled,
      updatedAt: now,
    })
    .where(eq(rolePermissionsTable.id, input.id));
    
  const [permission] = await db
    .select()
    .from(rolePermissionsTable)
    .where(eq(rolePermissionsTable.id, input.id))
    .limit(1);
    
  if (!permission) {
    throw new Error('Permission not found');
  }
  
  logger.info(`Updated role permission: ${permission.id}`);
  
  return {
    id: permission.id,
    role: permission.role,
    resource: permission.resource,
    action: permission.action,
    isEnabled: permission.isEnabled,
    createdAt: permission.createdAt,
    updatedAt: permission.updatedAt,
  };
}

/**
 * Bulk update role permissions with upsert logic (with error protection)
 * @param permissions - Array of permission updates with role/resource/action keys
 * @returns Updated permissions
 */
export async function bulkUpdateRolePermissions(permissions: UpsertRolePermissionInput[]): Promise<RolePermission[]> {
  const now = new Date();
  const results: RolePermission[] = [];
  const updatedRolesSet = new Set<string>();

  for (const input of permissions) {
    // Protection from corrupted data
    if (!input.role || !input.resource || !input.action) continue;

    updatedRolesSet.add(input.role);

    try {
      const [existing] = await db
        .select()
        .from(rolePermissionsTable)
        .where(
          and(
            eq(rolePermissionsTable.role, input.role),
            eq(rolePermissionsTable.resource, input.resource),
            eq(rolePermissionsTable.action, input.action)
          )
        )
        .limit(1);

      if (existing) {
        await db.update(rolePermissionsTable)
          .set({ isEnabled: input.isEnabled, updatedAt: now })
          .where(eq(rolePermissionsTable.id, existing.id));
      } else {
        await db.insert(rolePermissionsTable).values({
          id: crypto.randomUUID(),
          role: input.role,
          resource: input.resource,
          action: input.action,
          isEnabled: input.isEnabled,
          createdAt: now,
          updatedAt: now,
        });
      }
    } catch (err) {
      logger.error(`[System Service] Failed to upsert permission ${input.role}:${input.resource}:${input.action}`, err);
      // Ignore error for one record and continue the loop
    }
  }

  const updatedRoles = Array.from(updatedRolesSet);
  await emitEvent('system:permissions_updated', { roles: updatedRoles });

  logger.info(`Bulk upserted ${permissions.length} role permissions`);
  return db.select().from(rolePermissionsTable);
}

/**
 * Check if a role has a specific permission
 * @param role - The role to check
 * @param resource - The resource to check
 * @param action - The action to check
 * @returns True if permission exists and is enabled
 */
export async function checkRolePermission(role: string, resource: string, action: string): Promise<boolean> {
  const [permission] = await db
    .select()
    .from(rolePermissionsTable)
    .where(
      and(
        eq(rolePermissionsTable.role, role),
        eq(rolePermissionsTable.resource, resource),
        eq(rolePermissionsTable.action, action),
        eq(rolePermissionsTable.isEnabled, true)
      )
    )
    .limit(1);
    
  return !!permission;
}

/**
 * Get all system modules (with Smart Seeding)
 * @returns List of all system modules
 */
export async function getAllSystemModules(): Promise<SystemModule[]> {
  const modules = await db.select().from(systemModulesTable);
  
  // Smart Seeding: Check for missing modules
  const existingIds = modules.map(m => m.moduleId);
  const missingModules = REQUIRED_MODULES.filter(m => !existingIds.includes(m));

  if (missingModules.length > 0) {
    logger.info(`[System Service] Found ${missingModules.length} missing modules. Seeding...`);
    const now = new Date();
    
    for (const moduleId of missingModules) {
      await db.insert(systemModulesTable).values({
        id: crypto.randomUUID(),
        moduleId,
        isEnabled: true,
        createdAt: now,
        updatedAt: now,
      });
      logger.info(`[System Service] Seeded missing module: ${moduleId}`);
    }
    
    // Re-fetch full list after adding
    return db.select().from(systemModulesTable);
  }
  
  return modules.map(m => ({
    id: m.id,
    moduleId: m.moduleId,
    isEnabled: m.isEnabled,
    createdAt: m.createdAt,
    updatedAt: m.updatedAt,
  }));
}

/**
 * Check if a module is enabled
 * @param moduleId - The module ID to check
 * @returns True if module is enabled
 */
export async function isModuleEnabled(moduleId: string): Promise<boolean> {
  const [module] = await db
    .select()
    .from(systemModulesTable)
    .where(eq(systemModulesTable.moduleId, moduleId))
    .limit(1);
    
  return module?.isEnabled ?? false;
}

/**
 * Create a new system module
 * @param input - Module data
 * @returns Created module
 */
export async function createSystemModule(input: CreateSystemModuleInput): Promise<SystemModule> {
  const now = new Date();
  const id = crypto.randomUUID();
  
  await db
    .insert(systemModulesTable)
    .values({
      id,
      moduleId: input.moduleId,
      isEnabled: input.isEnabled ?? true,
      createdAt: now,
      updatedAt: now,
    });
    
  const [module] = await db
    .select()
    .from(systemModulesTable)
    .where(eq(systemModulesTable.id, id))
    .limit(1);
    
  logger.info(`Created system module: ${input.moduleId}`);
  
  return {
    id: module.id,
    moduleId: module.moduleId,
    isEnabled: module.isEnabled,
    createdAt: module.createdAt,
    updatedAt: module.updatedAt,
  };
}

/**
 * Update a system module
 * @param input - Module update data
 * @returns Updated module
 */
export async function updateSystemModule(input: UpdateSystemModuleInput): Promise<SystemModule> {
  const now = new Date();
  
  await db
    .update(systemModulesTable)
    .set({
      isEnabled: input.isEnabled,
      updatedAt: now,
    })
    .where(eq(systemModulesTable.id, input.id));
    
  const [module] = await db
    .select()
    .from(systemModulesTable)
    .where(eq(systemModulesTable.id, input.id))
    .limit(1);
    
  if (!module) {
    throw new Error('Module not found');
  }
  
  logger.info(`Updated system module: ${module.moduleId}`);
  
  return {
    id: module.id,
    moduleId: module.moduleId,
    isEnabled: module.isEnabled,
    createdAt: module.createdAt,
    updatedAt: module.updatedAt,
  };
}

/**
 * Bulk update system modules
 * @param modules - Array of module updates
 * @returns Updated modules
 */
export async function bulkUpdateSystemModules(modules: UpdateSystemModuleInput[]): Promise<SystemModule[]> {
  const now = new Date();
  const results: SystemModule[] = [];
  
  for (const input of modules) {
    await db
      .update(systemModulesTable)
      .set({
        isEnabled: input.isEnabled,
        updatedAt: now,
      })
      .where(eq(systemModulesTable.id, input.id));
      
    const [module] = await db
      .select()
      .from(systemModulesTable)
      .where(eq(systemModulesTable.id, input.id))
      .limit(1);
      
    if (module) {
      results.push({
        id: module.id,
        moduleId: module.moduleId,
        isEnabled: module.isEnabled,
        createdAt: module.createdAt,
        updatedAt: module.updatedAt,
      });
    }
  }
  
  logger.info(`Bulk updated ${results.length} system modules`);
  
  return results;
}

/**
 * Get all role color settings
 */
export async function getAllRoleColors(): Promise<RoleColors[]> {
  const colors = await db.select().from(roleColorsTable);

  return colors.map((item) => ({
    id: item.id,
    role: item.role,
    privateNormal: item.privateNormal,
    privateHigh: item.privateHigh,
    publicNormal: item.publicNormal,
    publicHigh: item.publicHigh,
    createdAt: item.createdAt,
    updatedAt: item.updatedAt,
  }));
}

/**
 * Upsert role colors for a specific role
 */
export async function upsertRoleColors(role: string, colors: UpsertRoleColorsInput): Promise<RoleColors> {
  const now = new Date();

  await db.insert(roleColorsTable)
    .values({
      id: crypto.randomUUID(),
      role,
      privateNormal: colors.privateNormal,
      privateHigh: colors.privateHigh,
      publicNormal: colors.publicNormal,
      publicHigh: colors.publicHigh,
      createdAt: now,
      updatedAt: now,
    })
    .onDuplicateKeyUpdate({
      set: {
        privateNormal: colors.privateNormal,
        privateHigh: colors.privateHigh,
        publicNormal: colors.publicNormal,
        publicHigh: colors.publicHigh,
        updatedAt: now,
      },
    });

  const [updated] = await db
    .select()
    .from(roleColorsTable)
    .where(eq(roleColorsTable.role, role))
    .limit(1);

  if (!updated) {
    throw new Error('Role colors not found after upsert');
  }

  return {
    id: updated.id,
    role: updated.role,
    privateNormal: updated.privateNormal,
    privateHigh: updated.privateHigh,
    publicNormal: updated.publicNormal,
    publicHigh: updated.publicHigh,
    createdAt: updated.createdAt,
    updatedAt: updated.updatedAt,
  };
}

const REQUIRED_ROLES = Object.values(USER_ROLES);

export interface RoleSessionPolicy {
  id: string;
  role: string;
  logoutTime: string | null;
  inactivityTimeout: number | null;
  sessionDuration: number | null;
  createdAt: Date;
  updatedAt: Date;
}

export interface UpsertSessionPolicyInput {
  role: string;
  logoutTime: string | null;
  inactivityTimeout: number | null;
  sessionDuration: number | null;
}

export async function getAllRoleSessionPolicies(): Promise<RoleSessionPolicy[]> {
  const policies = await db.select().from(roleSessionPoliciesTable);

  if (policies.length === 0) {
    logger.info('[System Service] Session policies empty. Running Smart Seeder...');
  }

  const existingRoles = new Set(policies.map((p) => p.role));
  const now = new Date();

  for (const role of REQUIRED_ROLES) {
    if (existingRoles.has(role)) continue;

    await db.insert(roleSessionPoliciesTable).values({
      id: crypto.randomUUID(),
      role,
      logoutTime: null,
      inactivityTimeout: 10,
      sessionDuration: null,
      createdAt: now,
      updatedAt: now,
    });

    logger.info(`[System Service] Seeded session policy for role: ${role}`);
  }

  const all = await db.select().from(roleSessionPoliciesTable);

  return all.map((p) => ({
    id: p.id,
    role: p.role,
    logoutTime: p.logoutTime,
    inactivityTimeout: p.inactivityTimeout,
    sessionDuration: p.sessionDuration,
    createdAt: p.createdAt,
    updatedAt: p.updatedAt,
  }));
}

export async function getSessionPolicyForRole(role: string): Promise<RoleSessionPolicy | null> {
  const [policy] = await db
    .select()
    .from(roleSessionPoliciesTable)
    .where(eq(roleSessionPoliciesTable.role, role))
    .limit(1);

  if (!policy) return null;

  return {
    id: policy.id,
    role: policy.role,
    logoutTime: policy.logoutTime,
    inactivityTimeout: policy.inactivityTimeout,
    sessionDuration: policy.sessionDuration,
    createdAt: policy.createdAt,
    updatedAt: policy.updatedAt,
  };
}

export async function bulkUpdateRoleSessionPolicies(
  inputs: UpsertSessionPolicyInput[],
): Promise<RoleSessionPolicy[]> {
  const now = new Date();

  for (const input of inputs) {
    if (!input.role) continue;

    const hasAny = input.logoutTime !== null || input.inactivityTimeout !== null || input.sessionDuration !== null;
    if (!hasAny) continue;

    try {
      const [existing] = await db
        .select()
        .from(roleSessionPoliciesTable)
        .where(eq(roleSessionPoliciesTable.role, input.role))
        .limit(1);

      if (existing) {
        await db
          .update(roleSessionPoliciesTable)
          .set({
            logoutTime: input.logoutTime,
            inactivityTimeout: input.inactivityTimeout,
            sessionDuration: input.sessionDuration,
            updatedAt: now,
          })
          .where(eq(roleSessionPoliciesTable.id, existing.id));
      } else {
        await db.insert(roleSessionPoliciesTable).values({
          id: crypto.randomUUID(),
          role: input.role,
          logoutTime: input.logoutTime,
          inactivityTimeout: input.inactivityTimeout,
          sessionDuration: input.sessionDuration,
          createdAt: now,
          updatedAt: now,
        });
      }
    } catch (err) {
      logger.error(`[System Service] Failed to upsert session policy for ${input.role}`, err);
    }
  }

  await emitEvent('session:policy_updated', { timestamp: Date.now() });
  invalidateSessionPolicyCache();

  logger.info(`[System Service] Bulk upserted ${inputs.length} session policies`);
  return db.select().from(roleSessionPoliciesTable).then((rows) =>
    rows.map((p) => ({
      id: p.id,
      role: p.role,
      logoutTime: p.logoutTime,
      inactivityTimeout: p.inactivityTimeout,
      sessionDuration: p.sessionDuration,
      createdAt: p.createdAt,
      updatedAt: p.updatedAt,
    })),
  );
}

const sessionPolicyCache = new Map<string, { policy: RoleSessionPolicy; ts: number }>();
const CACHE_TTL_MS = 60_000;

export function getCachedSessionPolicy(role: string): RoleSessionPolicy | null {
  const cached = sessionPolicyCache.get(role);
  if (cached && Date.now() - cached.ts < CACHE_TTL_MS) {
    return cached.policy;
  }
  return null;
}

export function setCachedSessionPolicy(role: string, policy: RoleSessionPolicy): void {
  sessionPolicyCache.set(role, { policy, ts: Date.now() });
}

export function invalidateSessionPolicyCache(): void {
  sessionPolicyCache.clear();
}
