import {
  Injectable,
  UnauthorizedException,
  ForbiddenException,
  ConflictException,
  Logger,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
import { SystemSettingsService } from '../system-settings/system-settings.service';
import { PrismaService } from '../../prisma/prisma.service';
import { CreateAdminDto } from './dto/create-admin.dto';
import { SaveBrandingDto } from './dto/save-branding.dto';

@Injectable()
export class SetupService {
  private readonly logger = new Logger(SetupService.name);

  constructor(
    private readonly systemSettings: SystemSettingsService,
    private readonly prisma: PrismaService,
    private readonly config: ConfigService,
    private readonly http: HttpService,
  ) {}

  /**
   * Returns the current setup status.
   */
  async getStatus() {
    const completed = await this.systemSettings.getValue('setup.completed');
    const tokenConfigured = await this.systemSettings.getValue('setup.token');

    return {
      completed: completed === 'true',
      tokenConfigured: !!tokenConfigured,
    };
  }

  /**
   * Creates the initial admin user in Keycloak and the local database.
   *
   * Flow:
   * 1. Validate the setup token
   * 2. Obtain a Keycloak admin access token via master realm
   * 3. Create the user in Keycloak with verified email and password
   * 4. Create/update the user in the local DB with the keycloakId reference
   * 5. Assign the "admin" role
   */
  async createAdmin(dto: CreateAdminDto, token: string): Promise<void> {
    await this.validateToken(token);

    const keycloakUrl = this.config.get<string>('KEYCLOAK_URL');
    const realm = this.config.get<string>('KEYCLOAK_REALM', 'tOS');
    const adminPassword = this.config.get<string>('KEYCLOAK_ADMIN_PASSWORD');

    // 1. Get admin access token from Keycloak master realm
    let adminToken: string;
    try {
      const tokenRes = await firstValueFrom(
        this.http.post(
          `${keycloakUrl}/realms/master/protocol/openid-connect/token`,
          new URLSearchParams({
            grant_type: 'password',
            client_id: 'admin-cli',
            username: 'admin',
            password: adminPassword ?? '',
          }),
          { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
        ),
      );
      adminToken = tokenRes.data.access_token;
    } catch (err) {
      this.logger.error('Failed to get Keycloak admin token', err);
      throw new Error('Keycloak admin authentication failed');
    }

    // 2. Create user in Keycloak
    let keycloakUserId: string;
    try {
      const createRes = await firstValueFrom(
        this.http.post(
          `${keycloakUrl}/admin/realms/${realm}/users`,
          {
            firstName: dto.firstName,
            lastName: dto.lastName,
            email: dto.email,
            username: dto.email,
            enabled: true,
            emailVerified: true,
            credentials: [
              { type: 'password', value: dto.password, temporary: false },
            ],
          },
          { headers: { Authorization: `Bearer ${adminToken}` } },
        ),
      );
      // Keycloak returns 201 with Location header containing the user ID
      const location = createRes.headers['location'] as string;
      keycloakUserId = location.split('/').pop() ?? '';
    } catch (err: any) {
      if (err.response?.status === 409) {
        throw new ConflictException(
          'A user with this email already exists in Keycloak',
        );
      }
      this.logger.error('Failed to create Keycloak user', err);
      throw new Error('Failed to create user in Keycloak');
    }

    // 3. Create user in local DB (keycloakId is the unique link to Keycloak)
    const adminRole = await this.prisma.role.findFirst({
      where: { name: 'admin' },
    });

    const user = await this.prisma.user.upsert({
      where: { keycloakId: keycloakUserId },
      create: {
        email: dto.email,
        firstName: dto.firstName,
        lastName: dto.lastName,
        keycloakId: keycloakUserId,
        isActive: true,
      },
      update: {
        email: dto.email,
        firstName: dto.firstName,
        lastName: dto.lastName,
      },
    });

    // 4. Assign admin role
    if (adminRole) {
      await this.prisma.userRole.upsert({
        where: {
          userId_roleId: { userId: user.id, roleId: adminRole.id },
        },
        create: { userId: user.id, roleId: adminRole.id },
        update: {},
      });
    }

    this.logger.log(
      `Admin user created: ${dto.email} (keycloakId: ${keycloakUserId})`,
    );
  }

  /**
   * Saves branding settings (app name, company name, logo URL).
   */
  async saveBranding(dto: SaveBrandingDto, token: string): Promise<void> {
    await this.validateToken(token);

    await this.systemSettings.upsert('branding.appName', {
      value: dto.appName,
      category: 'branding',
    });

    if (dto.companyName !== undefined) {
      await this.systemSettings.upsert('branding.companyName', {
        value: dto.companyName,
        category: 'branding',
      });
    }

    if (dto.logoUrl !== undefined) {
      await this.systemSettings.upsert('branding.logoUrl', {
        value: dto.logoUrl,
        category: 'branding',
      });
    }

    this.logger.log(`Branding settings saved: appName=${dto.appName}`);
  }

  /**
   * Marks the setup as completed. After this, all setup endpoints
   * will reject further modifications.
   */
  async completeSetup(token: string): Promise<void> {
    await this.validateToken(token);

    await this.systemSettings.upsert('setup.completed', {
      value: 'true',
      category: 'system',
      valueType: 'boolean',
    });

    this.logger.log('Setup completed successfully');
  }

  /**
   * Validates the setup token and checks that setup has not been completed yet.
   */
  private async validateToken(token: string): Promise<void> {
    const completed = await this.systemSettings.getValue('setup.completed');
    if (completed === 'true') {
      throw new ForbiddenException('Setup is already completed');
    }

    const storedToken = await this.systemSettings.getValue('setup.token');
    if (!storedToken || storedToken !== token) {
      throw new UnauthorizedException('Invalid setup token');
    }
  }
}
