import { Prisma } from '@prisma/client';
import { prisma } from '../../config/database';
import { logger } from '../../utils/logger';
import { siteSettingsService } from '../../modules/settings/settings.service';
import type {
  ServiceProvisioner,
  CMUser,
  ProvisionTiming,
  ServiceProvisioningStatus,
  ProvisionStatus,
} from './provisioner.interface';

/** Settings field → timing field mapping */
const TIMING_FIELDS: Record<string, string> = {
  provisionGitea: 'provisionGiteaTiming',
  provisionVaultwarden: 'provisionVaultwardenTiming',
  provisionListmonk: 'provisionListmonkTiming',
  enableChat: 'enableChat', // RC is always lazy (SSO on access)
};

class UserProvisioningService {
  private provisioners = new Map<string, ServiceProvisioner>();

  /** Register a provisioner (called at startup) */
  register(provisioner: ServiceProvisioner): void {
    this.provisioners.set(provisioner.config.serviceKey, provisioner);
    logger.info(`Provisioner registered: ${provisioner.config.serviceKey} (${provisioner.config.displayName})`);
  }

  /** Get all registered provisioners */
  getAll(): ServiceProvisioner[] {
    return Array.from(this.provisioners.values());
  }

  /** Get a specific provisioner by key */
  get(serviceKey: string): ServiceProvisioner | undefined {
    return this.provisioners.get(serviceKey);
  }

  // --- Lifecycle Hooks ---

  /**
   * Called after a user is created or approved.
   * Only provisions to services with 'eager' timing.
   */
  async onUserCreated(user: CMUser): Promise<void> {
    const settings = await this.getSettings();
    if (!settings.enableUserProvisioning) return;

    // Super Admins get all services provisioned immediately regardless of timing
    if (user.role === 'SUPER_ADMIN') {
      logger.info(`Auto-provisioning all services for SUPER_ADMIN user ${user.email}`);
      await this.provisionAll(user.id);
      return;
    }

    for (const provisioner of this.provisioners.values()) {
      try {
        if (this.shouldSkipUser(provisioner, user)) continue;
        if (!this.isServiceEnabled(provisioner, settings)) continue;

        const timing = this.getServiceTiming(provisioner, settings);
        if (timing !== 'eager') continue;

        const available = await provisioner.isAvailable();
        if (!available) {
          logger.warn(`Provisioner ${provisioner.config.serviceKey}: service unavailable, skipping eager provision`);
          await this.recordError(user.id, provisioner.config.serviceKey, 'Service unavailable');
          continue;
        }

        const result = await provisioner.provision(user);
        if (!result.success) {
          await this.recordError(user.id, provisioner.config.serviceKey, result.error || 'Unknown error');
        }
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        logger.error(`Provisioner ${provisioner.config.serviceKey} onUserCreated failed: ${msg}`);
        await this.recordError(user.id, provisioner.config.serviceKey, msg);
      }
    }
  }

  /**
   * Called after a user is updated (role change, name change, status change).
   * Syncs data to all services where the user is already provisioned.
   */
  async onUserUpdated(user: CMUser, changes: Record<string, unknown>): Promise<void> {
    const settings = await this.getSettings();
    if (!settings.enableUserProvisioning) return;

    const permissions = (user.permissions as Record<string, unknown>) || {};

    // Check if user status changed to deactivated
    const deactivatedStatuses = ['INACTIVE', 'SUSPENDED', 'EXPIRED'];
    if (changes.status && deactivatedStatuses.includes(changes.status as string)) {
      await this.onUserDeactivated(user);
      return;
    }

    // Promoted to Super Admin — auto-provision all services
    if ((changes.role || changes.roles) && user.role === 'SUPER_ADMIN') {
      logger.info(`Auto-provisioning all services for newly promoted SUPER_ADMIN ${user.email}`);
      await this.provisionAll(user.id);
      return;
    }

    for (const provisioner of this.provisioners.values()) {
      try {
        if (!this.isServiceEnabled(provisioner, settings)) continue;

        const serviceUserId = permissions[provisioner.config.permissionsKey] as string | undefined;
        if (!serviceUserId) continue; // Not provisioned to this service

        const available = await provisioner.isAvailable();
        if (!available) continue;

        await provisioner.syncUser(user, String(serviceUserId));
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        logger.warn(`Provisioner ${provisioner.config.serviceKey} sync failed for ${user.email}: ${msg}`);
        await this.recordError(user.id, provisioner.config.serviceKey, msg);
      }
    }
  }

  /**
   * Called when a user is deactivated or deleted.
   * Deactivates the user in all provisioned services.
   */
  async onUserDeactivated(user: CMUser): Promise<void> {
    const settings = await this.getSettings();
    if (!settings.enableUserProvisioning) return;

    const permissions = (user.permissions as Record<string, unknown>) || {};

    for (const provisioner of this.provisioners.values()) {
      try {
        if (!this.isServiceEnabled(provisioner, settings)) continue;

        const serviceUserId = permissions[provisioner.config.permissionsKey] as string | undefined;
        if (!serviceUserId) continue;

        const available = await provisioner.isAvailable();
        if (!available) continue;

        await provisioner.deactivate(String(serviceUserId));
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        logger.warn(`Provisioner ${provisioner.config.serviceKey} deactivate failed: ${msg}`);
        await this.recordError(user.id, provisioner.config.serviceKey, msg);
      }
    }
  }

  // --- On-Demand Provisioning ---

  /**
   * Provision a user to a specific service (lazy provisioning trigger).
   * Called when a user accesses a service for the first time.
   */
  async provisionForService(userId: string, serviceKey: string): Promise<{ success: boolean; error?: string }> {
    const provisioner = this.provisioners.get(serviceKey);
    if (!provisioner) return { success: false, error: `Unknown service: ${serviceKey}` };

    const settings = await this.getSettings();
    if (!settings.enableUserProvisioning) return { success: false, error: 'User provisioning is disabled' };
    if (!this.isServiceEnabled(provisioner, settings)) return { success: false, error: `${serviceKey} provisioning is disabled` };

    const user = await this.loadUser(userId);
    if (!user) return { success: false, error: 'User not found' };
    if (this.shouldSkipUser(provisioner, user)) return { success: false, error: 'User role excluded from this service' };

    const available = await provisioner.isAvailable();
    if (!available) return { success: false, error: 'Service unavailable' };

    const result = await provisioner.provision(user);
    if (!result.success) {
      await this.recordError(userId, serviceKey, result.error || 'Unknown error');
    }
    return { success: result.success, error: result.error };
  }

  /**
   * Provision a user to ALL enabled services at once.
   * Already-provisioned services get a sync instead of re-provision.
   * Used by "Provision All" button and Super Admin auto-provisioning.
   */
  async provisionAll(userId: string): Promise<{ results: { serviceKey: string; displayName: string; success: boolean; error?: string }[] }> {
    const settings = await this.getSettings();
    if (!settings.enableUserProvisioning) {
      return { results: [{ serviceKey: '_global', displayName: 'User Provisioning', success: false, error: 'User provisioning is disabled' }] };
    }

    const results: { serviceKey: string; displayName: string; success: boolean; error?: string }[] = [];

    for (const provisioner of this.provisioners.values()) {
      try {
        if (!this.isServiceEnabled(provisioner, settings)) continue;

        // Reload user before each provisioner to get latest permissions
        const user = await this.loadUser(userId);
        if (!user) {
          results.push({ serviceKey: provisioner.config.serviceKey, displayName: provisioner.config.displayName, success: false, error: 'User not found' });
          continue;
        }

        if (this.shouldSkipUser(provisioner, user)) continue;

        const available = await provisioner.isAvailable();
        if (!available) {
          results.push({ serviceKey: provisioner.config.serviceKey, displayName: provisioner.config.displayName, success: false, error: 'Service unavailable' });
          await this.recordError(userId, provisioner.config.serviceKey, 'Service unavailable');
          continue;
        }

        const permissions = (user.permissions as Record<string, unknown>) || {};
        const serviceUserId = permissions[provisioner.config.permissionsKey] as string | undefined;

        if (serviceUserId) {
          // Already provisioned — sync instead
          await provisioner.syncUser(user, String(serviceUserId));
          results.push({ serviceKey: provisioner.config.serviceKey, displayName: provisioner.config.displayName, success: true });
        } else {
          // Not yet provisioned — provision
          const result = await provisioner.provision(user);
          if (!result.success) {
            await this.recordError(userId, provisioner.config.serviceKey, result.error || 'Unknown error');
          }
          results.push({ serviceKey: provisioner.config.serviceKey, displayName: provisioner.config.displayName, success: result.success, error: result.error });
        }
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        logger.error(`Provisioner ${provisioner.config.serviceKey} provisionAll failed: ${msg}`);
        await this.recordError(userId, provisioner.config.serviceKey, msg);
        results.push({ serviceKey: provisioner.config.serviceKey, displayName: provisioner.config.displayName, success: false, error: msg });
      }
    }

    return { results };
  }

  /**
   * Deprovision a user from a specific service.
   */
  async deprovisionFromService(userId: string, serviceKey: string): Promise<{ success: boolean; error?: string }> {
    const provisioner = this.provisioners.get(serviceKey);
    if (!provisioner) return { success: false, error: `Unknown service: ${serviceKey}` };

    const user = await this.loadUser(userId);
    if (!user) return { success: false, error: 'User not found' };

    const permissions = (user.permissions as Record<string, unknown>) || {};
    const serviceUserId = permissions[provisioner.config.permissionsKey] as string | undefined;
    if (!serviceUserId) return { success: false, error: 'User not provisioned to this service' };

    try {
      await provisioner.deactivate(String(serviceUserId));

      // Clear provisioning data from permissions
      const updatedPermissions = { ...permissions };
      delete updatedPermissions[provisioner.config.permissionsKey];
      // Clean up any related keys
      for (const key of Object.keys(updatedPermissions)) {
        if (key.startsWith(`_${serviceKey}`) || key.startsWith(`_${provisioner.config.permissionsKey.replace('_', '')}`)) {
          delete updatedPermissions[key];
        }
      }

      await prisma.user.update({
        where: { id: userId },
        data: { permissions: updatedPermissions as unknown as Prisma.InputJsonValue },
      });

      return { success: true };
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      return { success: false, error: msg };
    }
  }

  // --- Status Queries ---

  /**
   * Get provisioning status for a user across all services.
   */
  async getProvisioningStatus(userId: string): Promise<ServiceProvisioningStatus[]> {
    const user = await this.loadUser(userId);
    if (!user) return [];

    const settings = await this.getSettings();
    const permissions = (user.permissions as Record<string, unknown>) || {};
    const errors = (permissions._provisioningErrors as Record<string, string>) || {};
    const statuses: ServiceProvisioningStatus[] = [];

    for (const provisioner of this.provisioners.values()) {
      const enabled = this.isServiceEnabled(provisioner, settings);
      const serviceUserId = permissions[provisioner.config.permissionsKey] as string | number | undefined;

      let status: ProvisionStatus = 'not_provisioned';
      const details: Record<string, unknown> = {};

      if (serviceUserId) {
        status = 'synced';
        // Check for invite-based services
        const inviteSentKey = `${provisioner.config.permissionsKey.replace('UserId', 'InviteSentAt')}`;
        if (permissions[inviteSentKey]) {
          details.inviteSentAt = permissions[inviteSentKey];
          if (provisioner.config.serviceKey === 'vaultwarden') {
            status = 'invited'; // Vaultwarden users are "invited" until they set up their vault
          }
        }
      }

      if (errors[provisioner.config.serviceKey]) {
        status = 'error';
      }

      // Gather extra details
      const usernameKey = `_${provisioner.config.serviceKey === 'gitea' ? 'giteaUsername' : ''}`;
      if (permissions._giteaUsername && provisioner.config.serviceKey === 'gitea') {
        details.username = permissions._giteaUsername;
      }

      statuses.push({
        serviceKey: provisioner.config.serviceKey,
        displayName: provisioner.config.displayName,
        status: enabled ? status : 'not_provisioned',
        serviceUserId: serviceUserId ? String(serviceUserId) : undefined,
        lastError: errors[provisioner.config.serviceKey],
        details: Object.keys(details).length > 0 ? details : undefined,
      });
    }

    return statuses;
  }

  /**
   * Bulk sync: provision all active users to all enabled eager services.
   * Returns summary of results.
   */
  async bulkSync(): Promise<{ total: number; provisioned: number; errors: number; details: Record<string, { success: number; failed: number }> }> {
    const settings = await this.getSettings();
    if (!settings.enableUserProvisioning) {
      return { total: 0, provisioned: 0, errors: 0, details: {} };
    }

    const users = await prisma.user.findMany({
      where: { status: 'ACTIVE' },
      select: {
        id: true, email: true, name: true, role: true,
        roles: true, status: true, permissions: true,
      },
    });

    const details: Record<string, { success: number; failed: number }> = {};
    let totalProvisioned = 0;
    let totalErrors = 0;

    for (const provisioner of this.provisioners.values()) {
      if (!this.isServiceEnabled(provisioner, settings)) continue;

      const available = await provisioner.isAvailable();
      if (!available) {
        details[provisioner.config.serviceKey] = { success: 0, failed: 0 };
        continue;
      }

      let success = 0;
      let failed = 0;

      for (const user of users) {
        const cmUser = this.toCMUser(user);
        if (this.shouldSkipUser(provisioner, cmUser)) continue;

        const permissions = (user.permissions as Record<string, unknown>) || {};
        const alreadyProvisioned = !!permissions[provisioner.config.permissionsKey];
        if (alreadyProvisioned) {
          // Sync existing
          try {
            await provisioner.syncUser(cmUser, String(permissions[provisioner.config.permissionsKey]));
            success++;
            totalProvisioned++;
          } catch {
            failed++;
            totalErrors++;
          }
        } else {
          // Provision new
          try {
            const result = await provisioner.provision(cmUser);
            if (result.success) {
              success++;
              totalProvisioned++;
            } else {
              failed++;
              totalErrors++;
            }
          } catch {
            failed++;
            totalErrors++;
          }
        }
      }

      details[provisioner.config.serviceKey] = { success, failed };
    }

    return { total: users.length, provisioned: totalProvisioned, errors: totalErrors, details };
  }

  // --- Helpers ---

  private async getSettings(): Promise<Record<string, unknown>> {
    try {
      const settings = await siteSettingsService.get();
      return settings as unknown as Record<string, unknown>;
    } catch {
      return {};
    }
  }

  private isServiceEnabled(provisioner: ServiceProvisioner, settings: Record<string, unknown>): boolean {
    return !!settings[provisioner.config.featureFlag];
  }

  private getServiceTiming(provisioner: ServiceProvisioner, settings: Record<string, unknown>): ProvisionTiming {
    // RC is always lazy (SSO on access)
    if (provisioner.config.serviceKey === 'rocketchat') return 'lazy';

    const timingField = TIMING_FIELDS[provisioner.config.featureFlag];
    if (timingField) {
      const timing = settings[timingField] as string;
      if (timing === 'eager' || timing === 'lazy') return timing;
    }
    return 'lazy';
  }

  private shouldSkipUser(provisioner: ServiceProvisioner, user: CMUser): boolean {
    if (!provisioner.config.excludeRoles) return false;
    return provisioner.config.excludeRoles.includes(user.role);
  }

  private async loadUser(userId: string): Promise<CMUser | null> {
    const user = await prisma.user.findUnique({
      where: { id: userId },
      select: {
        id: true, email: true, name: true, role: true,
        roles: true, status: true, permissions: true,
      },
    });
    return user ? this.toCMUser(user) : null;
  }

  private toCMUser(user: {
    id: string; email: string; name: string | null; role: string;
    roles: unknown; status: string; permissions: unknown;
  }): CMUser {
    return {
      id: user.id,
      email: user.email,
      name: user.name,
      role: user.role,
      roles: user.roles,
      status: user.status,
      permissions: user.permissions as Record<string, unknown> | null,
    };
  }

  /** Record a provisioning error in the user's permissions JSON */
  private async recordError(userId: string, serviceKey: string, error: string): Promise<void> {
    try {
      const user = await prisma.user.findUnique({
        where: { id: userId },
        select: { permissions: true },
      });
      const permissions = (user?.permissions as Record<string, unknown>) || {};
      const errors = (permissions._provisioningErrors as Record<string, string>) || {};
      errors[serviceKey] = error;

      await prisma.user.update({
        where: { id: userId },
        data: { permissions: { ...permissions, _provisioningErrors: errors } as unknown as Prisma.InputJsonValue },
      });
    } catch (err) {
      logger.error(`Failed to record provisioning error for user ${userId}:`, err);
    }
  }
}

export const userProvisioningService = new UserProvisioningService();
