import { SystemConfig } from '@app/infra/entities';
import {
  BadRequestException,
  Inject,
  Injectable,
  InternalServerErrorException,
  Logger,
  UnauthorizedException,
} from '@nestjs/common';
import cookieParser from 'cookie';
import { IncomingHttpHeaders } from 'http';
import { IKeyRepository } from '../api-key';
import { ICryptoRepository } from '../crypto/crypto.repository';
import { OAuthCore } from '../oauth/oauth.core';
import { ISharedLinkRepository } from '../shared-link';
import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config';
import { IUserRepository, UserCore } from '../user';
import { IUserTokenRepository, UserTokenCore } from '../user-token';
import { AuthType, IMMICH_ACCESS_COOKIE, IMMICH_API_KEY_HEADER } from './auth.constant';
import { AuthCore, LoginDetails } from './auth.core';
import { AuthUserDto, ChangePasswordDto, LoginCredentialDto, SignUpDto } from './dto';
import {
  AdminSignupResponseDto,
  AuthDeviceResponseDto,
  LoginResponseDto,
  LogoutResponseDto,
  mapAdminSignupResponse,
  mapUserToken,
} from './response-dto';

@Injectable()
export class AuthService {
  private userTokenCore: UserTokenCore;
  private authCore: AuthCore;
  private oauthCore: OAuthCore;
  private userCore: UserCore;

  private logger = new Logger(AuthService.name);

  constructor(
    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
    @Inject(IUserRepository) userRepository: IUserRepository,
    @Inject(IUserTokenRepository) userTokenRepository: IUserTokenRepository,
    @Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
    @Inject(IKeyRepository) private keyRepository: IKeyRepository,
    @Inject(INITIAL_SYSTEM_CONFIG)
    initialConfig: SystemConfig,
  ) {
    this.userTokenCore = new UserTokenCore(cryptoRepository, userTokenRepository);
    this.authCore = new AuthCore(cryptoRepository, configRepository, userTokenRepository, initialConfig);
    this.oauthCore = new OAuthCore(configRepository, initialConfig);
    this.userCore = new UserCore(userRepository, cryptoRepository);
  }

  public async login(
    loginCredential: LoginCredentialDto,
    loginDetails: LoginDetails,
  ): Promise<{ response: LoginResponseDto; cookie: string[] }> {
    if (!this.authCore.isPasswordLoginEnabled()) {
      throw new UnauthorizedException('Password login has been disabled');
    }

    let user = await this.userCore.getByEmail(loginCredential.email, true);
    if (user) {
      const isAuthenticated = this.authCore.validatePassword(loginCredential.password, user);
      if (!isAuthenticated) {
        user = null;
      }
    }

    if (!user) {
      this.logger.warn(
        `Failed login attempt for user ${loginCredential.email} from ip address ${loginDetails.clientIp}`,
      );
      throw new BadRequestException('Incorrect email or password');
    }

    return this.authCore.createLoginResponse(user, AuthType.PASSWORD, loginDetails);
  }

  public async logout(authUser: AuthUserDto, authType: AuthType): Promise<LogoutResponseDto> {
    if (authUser.accessTokenId) {
      await this.userTokenCore.delete(authUser.id, authUser.accessTokenId);
    }

    if (authType === AuthType.OAUTH) {
      const url = await this.oauthCore.getLogoutEndpoint();
      if (url) {
        return { successful: true, redirectUri: url };
      }
    }

    return { successful: true, redirectUri: '/auth/login?autoLaunch=0' };
  }

  public async changePassword(authUser: AuthUserDto, dto: ChangePasswordDto) {
    const { password, newPassword } = dto;
    const user = await this.userCore.getByEmail(authUser.email, true);
    if (!user) {
      throw new UnauthorizedException();
    }

    const valid = this.authCore.validatePassword(password, user);
    if (!valid) {
      throw new BadRequestException('Wrong password');
    }

    return this.userCore.updateUser(authUser, authUser.id, { password: newPassword });
  }

  public async adminSignUp(dto: SignUpDto): Promise<AdminSignupResponseDto> {
    const adminUser = await this.userCore.getAdmin();

    if (adminUser) {
      throw new BadRequestException('The server already has an admin');
    }

    try {
      const admin = await this.userCore.createUser({
        isAdmin: true,
        email: dto.email,
        firstName: dto.firstName,
        lastName: dto.lastName,
        password: dto.password,
        storageLabel: 'admin',
      });

      return mapAdminSignupResponse(admin);
    } catch (error) {
      this.logger.error(`Unable to register admin user: ${error}`, (error as Error).stack);
      throw new InternalServerErrorException('Failed to register new admin user');
    }
  }

  public async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthUserDto | null> {
    const shareKey = (headers['x-immich-share-key'] || params.key) as string;
    const userToken = (headers['x-immich-user-token'] ||
      params.userToken ||
      this.getBearerToken(headers) ||
      this.getCookieToken(headers)) as string;
    const apiKey = (headers[IMMICH_API_KEY_HEADER] || params.apiKey) as string;

    if (shareKey) {
      return this.validateSharedLink(shareKey);
    }

    if (userToken) {
      return this.userTokenCore.validate(userToken);
    }

    if (apiKey) {
      return this.validateApiKey(apiKey);
    }

    throw new UnauthorizedException('Authentication required');
  }

  async getDevices(authUser: AuthUserDto): Promise<AuthDeviceResponseDto[]> {
    const userTokens = await this.userTokenCore.getAll(authUser.id);
    return userTokens.map((userToken) => mapUserToken(userToken, authUser.accessTokenId));
  }

  async logoutDevice(authUser: AuthUserDto, deviceId: string): Promise<void> {
    await this.userTokenCore.delete(authUser.id, deviceId);
  }

  async logoutDevices(authUser: AuthUserDto): Promise<void> {
    const devices = await this.userTokenCore.getAll(authUser.id);
    for (const device of devices) {
      if (device.id === authUser.accessTokenId) {
        continue;
      }
      await this.userTokenCore.delete(authUser.id, device.id);
    }
  }

  private getBearerToken(headers: IncomingHttpHeaders): string | null {
    const [type, token] = (headers.authorization || '').split(' ');
    if (type.toLowerCase() === 'bearer') {
      return token;
    }

    return null;
  }

  private getCookieToken(headers: IncomingHttpHeaders): string | null {
    const cookies = cookieParser.parse(headers.cookie || '');
    return cookies[IMMICH_ACCESS_COOKIE] || null;
  }

  private async validateSharedLink(key: string | string[]): Promise<AuthUserDto> {
    key = Array.isArray(key) ? key[0] : key;

    const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url');
    const link = await this.sharedLinkRepository.getByKey(bytes);
    if (link) {
      if (!link.expiresAt || new Date(link.expiresAt) > new Date()) {
        const user = link.user;
        if (user) {
          return {
            id: user.id,
            email: user.email,
            isAdmin: user.isAdmin,
            isPublicUser: true,
            sharedLinkId: link.id,
            isAllowUpload: link.allowUpload,
            isAllowDownload: link.allowDownload,
            isShowExif: link.showExif,
          };
        }
      }
    }
    throw new UnauthorizedException('Invalid share key');
  }

  private async validateApiKey(key: string): Promise<AuthUserDto> {
    const hashedKey = this.cryptoRepository.hashSha256(key);
    const keyEntity = await this.keyRepository.getKey(hashedKey);
    if (keyEntity?.user) {
      const user = keyEntity.user;

      return {
        id: user.id,
        email: user.email,
        isAdmin: user.isAdmin,
        isPublicUser: false,
        isAllowUpload: true,
        externalPath: user.externalPath,
      };
    }

    throw new UnauthorizedException('Invalid API key');
  }
}
