// RBAC Engine Service
// Core permission checking, screen visibility, and action authorization

import fs from 'fs';
import path from 'path';
import prisma from '@/shared/database/prisma';
import {
  PermissionSchema,
  PermissionCheckResult,
  ScreenVisibilityResult,
  Action,
  ScreenName,
  ParticipantType,
  RoleDefinition,
  AuditLogEntry,
  ResourceContext,
} from './permission-types';
import { logger } from '@/infrastructure/monitoring/logger';

export class RbacEngineService {
  private schema: PermissionSchema | null = null;
  private schemaPath: string;

  constructor() {
    // Schema file path - works in both development and production
    // In development: src/core/operations/permissions/permission-schema.json
    // In production: dist/core/operations/permissions/permission-schema.json (if copied)
    // Fallback to src path for development
    const devPath = path.join(process.cwd(), 'src', 'core', 'operations', 'permissions', 'permission-schema.json');
    const prodPath = typeof __dirname !== 'undefined' 
      ? path.join(__dirname, 'permission-schema.json')
      : devPath;
    
    // Prefer production path if it exists, otherwise use dev path
    this.schemaPath = fs.existsSync(prodPath) ? prodPath : devPath;
    this.loadSchema();
  }

  /**
   * Load permission schema from JSON file
   */
  private loadSchema(): void {
    try {
      const schemaContent = fs.readFileSync(this.schemaPath, 'utf-8');
      this.schema = JSON.parse(schemaContent) as PermissionSchema;
      logger.info('Permission schema loaded successfully');
    } catch (error) {
      logger.error('Failed to load permission schema', {
        error: error instanceof Error ? error.message : 'Unknown error',
        path: this.schemaPath,
      });
      throw new Error('Permission schema not found or invalid');
    }
  }

  /**
   * Get role definition from schema
   */
  private getRoleDefinition(
    participant: ParticipantType,
    roleName: string
  ): RoleDefinition | null {
    if (!this.schema) {
      return null;
    }

    const roleDef = this.schema.roles[participant]?.[roleName];
    if (!roleDef) {
      return null;
    }

    // Handle role inheritance
    if (roleDef.inheritsFrom.length > 0) {
      const mergedRole = { ...roleDef };
      for (const inheritedRoleName of roleDef.inheritsFrom) {
        const inheritedRole = this.getRoleDefinition(participant, inheritedRoleName);
        if (inheritedRole) {
          // Merge screen access (OR logic)
          Object.keys(inheritedRole.screenAccess).forEach((screen) => {
            mergedRole.screenAccess[screen as ScreenName] =
              mergedRole.screenAccess[screen as ScreenName] ||
              inheritedRole.screenAccess[screen as ScreenName];
          });

          // Merge actions (union of actions)
          Object.keys(inheritedRole.actions).forEach((module) => {
            if (!mergedRole.actions[module]) {
              mergedRole.actions[module] = [];
            }
            const existingActions = new Set(mergedRole.actions[module]);
            inheritedRole.actions[module].forEach((action) => {
              existingActions.add(action);
            });
            mergedRole.actions[module] = Array.from(existingActions) as Action[];
          });
        }
      }
      return mergedRole;
    }

    return roleDef;
  }

  /**
   * Get employee role and participant information
   */
  private async getEmployeeRoleInfo(employeeId: string): Promise<{
    roleName: string;
    participant: ParticipantType;
    accessLevel: string;
  } | null> {
    const employee = await prisma.employeeCredential.findUnique({
      where: { employeeId },
      include: { role: true },
    });

    if (!employee || employee.status !== 'active') {
      return null;
    }

    // Determine participant type from role name
    const roleName = employee.role.roleName;
    let participant: ParticipantType = 'DBIS';

    if (roleName.startsWith('SCB_')) {
      participant = 'SCB';
    } else if (roleName.startsWith('FI_')) {
      participant = 'FI';
    }

    return {
      roleName,
      participant,
      accessLevel: employee.role.accessLevel,
    };
  }

  /**
   * Check if employee has permission for a specific action on a resource
   */
  async checkPermission(
    employeeId: string,
    module: string,
    action: Action,
    context?: ResourceContext
  ): Promise<PermissionCheckResult> {
    const startTime = Date.now();

    try {
      const roleInfo = await this.getEmployeeRoleInfo(employeeId);
      if (!roleInfo) {
        await this.logAudit({
          employeeId,
          role: 'unknown',
          participant: 'DBIS',
          checkType: 'permission',
          resource: module,
          action,
          result: false,
          reason: 'Employee not found or inactive',
        });
        return {
          allowed: false,
          reason: 'Employee not found or inactive',
        };
      }

      const roleDef = this.getRoleDefinition(roleInfo.participant, roleInfo.roleName);
      if (!roleDef) {
        await this.logAudit({
          employeeId,
          role: roleInfo.roleName,
          participant: roleInfo.participant,
          checkType: 'permission',
          resource: module,
          action,
          result: false,
          reason: 'Role definition not found',
        });
        return {
          allowed: false,
          reason: 'Role definition not found',
        };
      }

      // Check if action is allowed
      const moduleActions = roleDef.actions[module] || roleDef.actions['*'] || [];
      const hasAction = moduleActions.includes(action) || moduleActions.includes('*');

      if (!hasAction) {
        await this.logAudit({
          employeeId,
          role: roleInfo.roleName,
          participant: roleInfo.participant,
          checkType: 'permission',
          resource: module,
          action,
          result: false,
          reason: 'Action not permitted for this role',
        });
        return {
          allowed: false,
          reason: 'Action not permitted for this role',
        };
      }

      // Check separation of duties
      if (roleDef.constraints.separationOfDuties.length > 0) {
        // This would need additional logic to check if conflicting roles are involved
        // For now, we'll log it but allow the action
        logger.debug('Separation of duties constraint exists', {
          employeeId,
          role: roleInfo.roleName,
          constraints: roleDef.constraints.separationOfDuties,
        });
      }

      // Check if approval is required
      let requiresApproval = false;
      let approverRole: string | undefined;

      if (roleDef.constraints.requiresApproval) {
        if (typeof roleDef.constraints.requiresApproval === 'object') {
          const approvalReq = roleDef.constraints.requiresApproval;
          if (context?.amount && approvalReq.amountThreshold) {
            if (context.amount >= approvalReq.amountThreshold) {
              requiresApproval = true;
              approverRole = approvalReq.approverRole;
            }
          }
        } else if (roleDef.constraints.requiresApproval === true) {
          requiresApproval = true;
        }
      }

      const result: PermissionCheckResult = {
        allowed: true,
        requiresApproval,
        approverRole,
      };

      await this.logAudit({
        employeeId,
        role: roleInfo.roleName,
        participant: roleInfo.participant,
        checkType: 'permission',
        resource: module,
        action,
        result: true,
        metadata: {
          requiresApproval,
          approverRole,
          duration: Date.now() - startTime,
        },
      });

      return result;
    } catch (error) {
      logger.error('Error checking permission', {
        error: error instanceof Error ? error.message : 'Unknown error',
        employeeId,
        module,
        action,
      });

      await this.logAudit({
        employeeId,
        role: 'unknown',
        participant: 'DBIS',
        checkType: 'permission',
        resource: module,
        action,
        result: false,
        reason: error instanceof Error ? error.message : 'Unknown error',
      });

      return {
        allowed: false,
        reason: 'Error checking permission',
      };
    }
  }

  /**
   * Check if employee can access a screen
   */
  async checkScreenAccess(
    employeeId: string,
    screenName: ScreenName
  ): Promise<ScreenVisibilityResult> {
    const startTime = Date.now();

    try {
      const roleInfo = await this.getEmployeeRoleInfo(employeeId);
      if (!roleInfo) {
        await this.logAudit({
          employeeId,
          role: 'unknown',
          participant: 'DBIS',
          checkType: 'screen',
          resource: screenName,
          result: false,
          reason: 'Employee not found or inactive',
        });
        return {
          visible: false,
          components: {},
        };
      }

      const roleDef = this.getRoleDefinition(roleInfo.participant, roleInfo.roleName);
      if (!roleDef) {
        await this.logAudit({
          employeeId,
          role: roleInfo.roleName,
          participant: roleInfo.participant,
          checkType: 'screen',
          resource: screenName,
          result: false,
          reason: 'Role definition not found',
        });
        return {
          visible: false,
          components: {},
        };
      }

      const screenVisible = roleDef.screenAccess[screenName] || false;

      // Get component visibility
      const components: { [componentName: string]: boolean } = {};
      if (screenVisible && this.schema?.screens[screenName]) {
        const screenDef = this.schema.screens[screenName];
        // For now, all components are visible if screen is visible
        // In future, we can add component-level permissions
        Object.keys(screenDef.components).forEach((componentName) => {
          components[componentName] = true;
        });
      }

      const result: ScreenVisibilityResult = {
        visible: screenVisible,
        components,
      };

      await this.logAudit({
        employeeId,
        role: roleInfo.roleName,
        participant: roleInfo.participant,
        checkType: 'screen',
        resource: screenName,
        result: screenVisible,
        metadata: {
          duration: Date.now() - startTime,
        },
      });

      return result;
    } catch (error) {
      logger.error('Error checking screen access', {
        error: error instanceof Error ? error.message : 'Unknown error',
        employeeId,
        screenName,
      });

      await this.logAudit({
        employeeId,
        role: 'unknown',
        participant: 'DBIS',
        checkType: 'screen',
        resource: screenName,
        result: false,
        reason: error instanceof Error ? error.message : 'Unknown error',
      });

      return {
        visible: false,
        components: {},
      };
    }
  }

  /**
   * Get all accessible screens for an employee
   */
  async getAccessibleScreens(employeeId: string): Promise<ScreenName[]> {
    const roleInfo = await this.getEmployeeRoleInfo(employeeId);
    if (!roleInfo) {
      return [];
    }

    const roleDef = this.getRoleDefinition(roleInfo.participant, roleInfo.roleName);
    if (!roleDef) {
      return [];
    }

    const accessibleScreens: ScreenName[] = [];
    Object.keys(roleDef.screenAccess).forEach((screen) => {
      if (roleDef.screenAccess[screen as ScreenName]) {
        accessibleScreens.push(screen as ScreenName);
      }
    });

    return accessibleScreens;
  }

  /**
   * Get all allowed actions for a module
   */
  async getAllowedActions(
    employeeId: string,
    module: string
  ): Promise<Action[]> {
    const roleInfo = await this.getEmployeeRoleInfo(employeeId);
    if (!roleInfo) {
      return [];
    }

    const roleDef = this.getRoleDefinition(roleInfo.participant, roleInfo.roleName);
    if (!roleDef) {
      return [];
    }

    return roleDef.actions[module] || roleDef.actions['*'] || [];
  }

  /**
   * Log audit entry
   */
  private async logAudit(entry: Omit<AuditLogEntry, 'timestamp'>): Promise<void> {
    if (!this.schema?.audit.logAllPermissionChecks) {
      return;
    }

    try {
      const auditEntry: AuditLogEntry = {
        timestamp: new Date(),
        ...entry,
      };

      // In a production system, this would write to a database or audit log service
      // For now, we'll just log it
      logger.debug('Permission audit log', {
        ...auditEntry,
        timestamp: auditEntry.timestamp.toISOString(),
      });

      // TODO: Implement persistent audit logging to database
      // await prisma.auditLog.create({ data: { ... } });
    } catch (error) {
      logger.error('Failed to log audit entry', {
        error: error instanceof Error ? error.message : 'Unknown error',
        entry,
      });
    }
  }

  /**
   * Reload schema (useful for hot-reloading in development)
   */
  reloadSchema(): void {
    this.loadSchema();
    logger.info('Permission schema reloaded');
  }

  /**
   * Get role definition for external use
   */
  getRoleDefinitionForRole(
    participant: ParticipantType,
    roleName: string
  ): RoleDefinition | null {
    return this.getRoleDefinition(participant, roleName);
  }

  /**
   * Get all roles for a participant
   */
  getRolesForParticipant(participant: ParticipantType): string[] {
    if (!this.schema) {
      return [];
    }
    return this.schema.participants[participant]?.roles || [];
  }

  /**
   * Validate permission schema structure
   */
  validateSchema(): { valid: boolean; errors: string[] } {
    const errors: string[] = [];

    if (!this.schema) {
      errors.push('Schema is null or not loaded');
      return { valid: false, errors };
    }

    // Validate participants
    const requiredParticipants: ParticipantType[] = ['DBIS', 'SCB', 'FI'];
    requiredParticipants.forEach((participant) => {
      if (!this.schema!.participants[participant]) {
        errors.push(`Missing participant: ${participant}`);
      }
    });

    // Validate roles exist for each participant
    Object.keys(this.schema.roles).forEach((participant) => {
      const participantRoles = this.schema!.participants[participant as ParticipantType]?.roles || [];
      participantRoles.forEach((roleName) => {
        if (!this.schema!.roles[participant]?.[roleName]) {
          errors.push(`Missing role definition: ${participant}.${roleName}`);
        }
      });
    });

    return {
      valid: errors.length === 0,
      errors,
    };
  }
}

export const rbacEngineService = new RbacEngineService();

