import { getLocaleText } from '@hed-hog/api-locale';
import { PrismaService, user_account_provider_52222e2ecb_enum } from '@hed-hog/api-prisma';
import {
  BadRequestException,
  ConflictException,
  Inject,
  Injectable,
  Logger,
  NotFoundException,
  ServiceUnavailableException,
  forwardRef
} from '@nestjs/common';
import { AuthService } from '../auth/auth.service';
import { FileService } from '../file/file.service';
import { MailService } from '../mail/mail.service';
import { SecurityService } from '../security/security.service';
import { SettingService } from '../setting/setting.service';
import { TokenService } from '../token/token.service';
import { USER_AVATAR_UPLOAD_DESTINATION } from '../user/constants/user.constants';
import { UserService } from '../user/user.service';
import { OAuthProvider } from './interfaces/OAuthProvider';
import { OAuthCallbackCoordinatorService } from './oauth-callback-coordinator.service';
import {
  OAuthCallbackAlreadyProcessedError,
  OAuthCallbackInProgressError,
  OAuthProviderException,
} from './oauth.errors';
import { FacebookProvider } from './providers/facebook.provider';
import { GithubProvider } from './providers/github.provider';
import { GoogleProvider } from './providers/google.provider';
import { MicrosoftEntraIdProvider } from './providers/microsoft-entra-id.provider';
import { MicrosoftProvider } from './providers/microsoft.provider';

type HandleCallbackProps = {
  locale: string;
  ipAddress: string; 
  userAgent: string;
  res: Response;
  provider: user_account_provider_52222e2ecb_enum;
  code?: string;
  type: 'login' | 'register' | 'connect' | 'disconnect';
  userId?: number;
  email?: string;
};

@Injectable()
export class OAuthService {
  private readonly logger = new Logger(OAuthService.name);
  private providers: Map<string, OAuthProvider> = new Map();

  constructor(
    google: GoogleProvider,
    facebook: FacebookProvider,
    microsoft: MicrosoftProvider,
    github: GithubProvider,
    microsoftEntraId: MicrosoftEntraIdProvider,
    private readonly callbackCoordinator: OAuthCallbackCoordinatorService,
    @Inject(forwardRef(() => AuthService))
    private readonly auth: AuthService,
    private readonly file: FileService,
    private readonly mail: MailService,
    private readonly security: SecurityService,
    private readonly prisma: PrismaService,
    private readonly token: TokenService,
    private readonly setting: SettingService,
    @Inject(forwardRef(() => UserService))
    private readonly user: UserService,
  ) {
    this.providers.set(google.getProviderType(), google);
    this.providers.set(facebook.getProviderType(), facebook);
    this.providers.set(microsoft.getProviderType(), microsoft);
    this.providers.set(github.getProviderType(), github);
    this.providers.set(microsoftEntraId.getProviderType(), microsoftEntraId);
  }

  getAuthUrl(provider: user_account_provider_52222e2ecb_enum, callbackPath: string) {
    return this.getProvider(provider).getAuthUrl(callbackPath);
  }

  async handleCallback({
    res,
    locale,
    ipAddress,
    userAgent,
    provider,
    code,
    type,
    userId,
    email,
  }: HandleCallbackProps) {
    try {
      this.logger.log(
        `OAuth callback received: provider=${provider} type=${type} locale=${locale} ip=${ipAddress || 'n/a'} hasCode=${code ? 'yes' : 'no'} codeLength=${code?.length ?? 0} userAgent="${this.summarizeUserAgent(userAgent)}"`,
      );

      if (type === 'disconnect') {
        return this.handleDisconnect(res, locale, ipAddress, userAgent, provider, email);
      }

      if (!code) {
        throw new BadRequestException(
          getLocaleText(
            'oauth.callback.codeRequired',
            locale,
            'Authorization code is required to complete OAuth authentication.',
          ),
        );
      }

      const callbackKey = this.buildCallbackKey(provider, type, code);

      return await this.callbackCoordinator.execute(callbackKey, async () => {
        const profile = await this.getProvider(provider).getProfile(code, type);
        const result = await this.finalizeCallback({
          res,
          locale,
          ipAddress,
          userAgent,
          provider,
          type,
          profile,
          userId,
        });

        this.logger.log(
          `OAuth callback completed: provider=${provider} type=${type} profileEmailPresent=${profile?.email ? 'yes' : 'no'} userId=${userId ?? 'n/a'}`,
        );

        return result;
      });
    } catch (error) {
      this.logger.warn(
        `OAuth callback failed before response mapping: provider=${provider} type=${type} message=${error instanceof Error ? error.message : 'unknown error'}`,
      );
      throw this.mapCallbackError(locale, provider, type, error);
    }
  }

  private getProvider(provider: user_account_provider_52222e2ecb_enum) {
    const prov = this.providers.get(String(provider).toUpperCase());
    if (!prov)
      throw new BadRequestException(`Provider ${provider} não suportado`);
    return prov;
  }

  private async getProviderScopes(provider: user_account_provider_52222e2ecb_enum) {
    const settingKey = `${String(provider).toLowerCase().replace(/-/g, '_')}_scopes`;

    const settings = await this.setting.getSettingValues([
      'oauth-mfa',
      'google_scopes',
      'facebook_scopes',
      'github_scopes',
      'microsoft_scopes',
      'microsoft_entra_id_scopes',
    ]);

    return settings[settingKey].join(',') || '';
  }

  private buildCallbackKey(
    provider: user_account_provider_52222e2ecb_enum,
    type: HandleCallbackProps['type'],
    code: string,
  ) {
    return `${String(provider).toLowerCase()}:${type}:${this.security.hashSha256(code)}`;
  }

  private summarizeUserAgent(userAgent?: string) {
    if (!userAgent) {
      return 'n/a';
    }

    return userAgent.length > 120
      ? `${userAgent.slice(0, 117)}...`
      : userAgent;
  }

  private async finalizeCallback({
    res,
    locale,
    ipAddress,
    userAgent,
    provider,
    type,
    profile,
    userId,
  }: Omit<HandleCallbackProps, 'code' | 'email'> & { profile: any }) {
    switch (type) {
      case 'login':
        return this.handleLogin(res, locale, ipAddress, userAgent, provider, profile);
      case 'register':
        return this.handleRegister(res, locale, ipAddress, userAgent, provider, profile);
      case 'connect':
        return this.handleConnect(res, locale, ipAddress, userAgent, provider, profile, userId);
      default:
        throw new BadRequestException(
          getLocaleText('oauth.callback.invalidType', locale, 'Unsupported OAuth callback type.'),
        );
    }
  }

  private resolveProfileScopes(provider: user_account_provider_52222e2ecb_enum, profile: any) {
    if (typeof profile?.oauth_scopes === 'string') {
      return profile.oauth_scopes;
    }

    return this.getProviderScopes(provider);
  }

  private mapCallbackError(
    locale: string,
    provider: user_account_provider_52222e2ecb_enum,
    type: HandleCallbackProps['type'],
    error: unknown,
  ) {
    if (
      error instanceof BadRequestException ||
      error instanceof ConflictException ||
      error instanceof NotFoundException ||
      error instanceof ServiceUnavailableException
    ) {
      return error;
    }

    if (error instanceof OAuthCallbackAlreadyProcessedError) {
      return new BadRequestException(
        getLocaleText(
          'oauth.callback.alreadyProcessed',
          locale,
          'This OAuth callback has already been processed. Start the sign-in flow again.',
        ),
      );
    }

    if (error instanceof OAuthCallbackInProgressError) {
      return new ConflictException(
        getLocaleText(
          'oauth.callback.inProgress',
          locale,
          'This OAuth callback is already being processed. Wait a moment and try again.',
        ),
      );
    }

    if (error instanceof OAuthProviderException) {
      if (error.reason === 'callback_consumed' || error.reason === 'invalid_callback') {
        return new BadRequestException(getLocaleText('oauth.callback.invalid', locale, error.message));
      }

      this.logger.warn(
        `OAuth callback upstream failure: provider=${provider} type=${type} status=${error.status ?? 'unknown'} reason=${error.reason} providerError=${error.providerError ?? 'unknown'} providerCode=${error.providerErrorCode ?? 'n/a'}`,
      );

      return new ServiceUnavailableException(
        getLocaleText(
          'oauth.callback.unavailable',
          locale,
          'Unable to complete OAuth authentication right now. Please try again.',
        ),
      );
    }

    this.logger.error(
      `Unexpected OAuth callback failure: provider=${provider} type=${type} error=${error instanceof Error ? error.message : 'unknown'}`,
    );

    return new ServiceUnavailableException(
      getLocaleText(
        'oauth.callback.unavailable',
        locale,
        'Unable to complete OAuth authentication right now. Please try again.',
      ),
    );
  }

  private async handleLogin(res: Response, locale: string, ipAddress: string, userAgent: string, provider: user_account_provider_52222e2ecb_enum, profile: any) {
    const userAccount = await this.prisma.user_account.findFirst({
      where: {
        provider: String(provider).toLowerCase().replace(/-/g, '_') as user_account_provider_52222e2ecb_enum,
        email: profile.email,
      },
      include: {
        user: true,
      },
    });

    if (!userAccount) {
      const userIdentifier = await this.prisma.user_identifier.findFirst({
        where: {
          type: 'email',
          value: profile.email,
          enabled: true
        },
        include: { user: true },
      });

      if (userIdentifier) {
        return this.handleConnect(res, locale, ipAddress, userAgent, provider, profile, userIdentifier.user_id);
      } else {
        return this.handleRegister(res, locale, ipAddress, userAgent, provider, profile);
      }
    } else {
      if (profile.oauth_tokens?.refresh_token) {
        const encryptedRefreshToken = this.security.encrypt(profile.oauth_tokens.refresh_token);
        const scopes = await this.resolveProfileScopes(provider, profile);
        await this.prisma.user_account.update({
          where: { id: userAccount.id },
          data: {
            refresh_token: Buffer.from(encryptedRefreshToken),
            token_expires_at: profile.oauth_tokens.expires_in
              ? new Date(Date.now() + profile.oauth_tokens.expires_in * 1000)
              : null,
            scopes,
          },
        });
      }

      const { accessToken, refreshToken, session } = await this.auth.getAuthenticationPayload(
        locale,
        userAccount.user_id,
        ipAddress,
        userAgent
      );

      await this.token.setRefreshTokenCookie(locale, res, refreshToken, session.expires_at);
      return { accessToken };
    }
  }

  private async handleRegister(res: Response, locale: string, ipAddress: string, userAgent: string, provider: user_account_provider_52222e2ecb_enum, profile: any) {
    const existingUser = await this.prisma.user_identifier.findFirst({
      where: {
        type: 'email',
        value: profile.email,
      },
    });

    if (existingUser) {
      throw new ConflictException(
        'Este email já está sendo usado. Entre em sua conta e utilize a opção "Conectar".',
      );
    }

    const encryptedRefreshToken = profile.oauth_tokens.refresh_token ? this.security.encrypt(profile.oauth_tokens.refresh_token) : null;
    const scopes = await this.resolveProfileScopes(provider, profile);

    const settings = await this.setting.getSettingValues([
      'oauth-role-assignment',
    ]);

    const roles = settings['oauth-role-assignment'] || [];

    const roleIds = this.prisma.role.findMany({
      where: { 
        slug: { in: roles }        
       },
       select: { id: true }
    });

    const user = await this.prisma.user.create({
      data: {
        name: profile.name,
        role_user: {
          create: (await roleIds).map((role) => ({ role_id: role.id })),
        },
        user_identifier: {
          create: {
            type: 'email',
            value: profile.email,
            verified_at: new Date(),
            enabled: true
          },
        },
        user_account: {
          create: {
            provider: String(provider).toLowerCase().replace(/-/g, '_') as user_account_provider_52222e2ecb_enum,
            provider_user_id: profile.id,
            email: profile.email,
            refresh_token: encryptedRefreshToken ? Buffer.from(encryptedRefreshToken) : null,
            token_expires_at: profile.oauth_tokens?.expires_in
              ? new Date(Date.now() + profile.oauth_tokens.expires_in * 1000)
              : null,
            scopes,
          },
        },
      },
    });

    await this.mail.sendTemplatedMail(locale, {
      email: profile.email,
      slug: 'auth-sign-up',
      variables: { name: profile.name },
    });

    const pictureURL = profile.picture?.data?.url || profile.picture;
    if (pictureURL) {
      const photoId = await this.downloadAndSaveProfilePicture(
        pictureURL,
        user.id,
        profile.oauth_tokens?.access_token,
      );

      await this.prisma.user.update({
        where: { id: user.id },
        data: { photo_id: photoId },
      });
    }

    await this.user.registerUserActivity(user.id, 'oauth-register');
    const { accessToken, refreshToken, session } = await this.auth.getAuthenticationPayload(
      locale,
      user.id,
      ipAddress,
      userAgent
    );
    await this.token.setRefreshTokenCookie(locale, res, refreshToken, session.expires_at);
    return { accessToken };
  }

  private async handleConnect(
    res: Response, 
    locale: string, 
    ipAddress: string, 
    userAgent: string,
    provider: user_account_provider_52222e2ecb_enum,
    profile: any,
    userId: number,
  ) {
    const existingAccount = await this.prisma.user_account.findFirst({
      where: {
        user_id: userId,
        provider: String(provider).toLowerCase().replace(/-/g, '_') as user_account_provider_52222e2ecb_enum,
      },
    });

    if (existingAccount) {
      throw new BadRequestException(`Usuário já conectado com o ${provider}.`);
    }

    const encryptedRefreshToken = profile.oauth_tokens.refresh_token ? this.security.encrypt(profile.oauth_tokens.refresh_token) : null;
    const scopes = await this.resolveProfileScopes(provider, profile);
    await this.prisma.user_account.create({
      data: {
        user_id: userId,
        provider: String(provider).toLowerCase().replace(/-/g, '_') as user_account_provider_52222e2ecb_enum,
        provider_user_id: profile.id,
        email: profile.email,
        refresh_token: encryptedRefreshToken ? Buffer.from(encryptedRefreshToken) : null,
        token_expires_at: profile.oauth_tokens?.expires_in
          ? new Date(Date.now() + profile.oauth_tokens.expires_in * 1000)
          : null,
        scopes,
      },
    });

    const user = await this.prisma.user.findUnique({ where: { id: userId } });
    const updateData: any = {};
    const pictureURL = profile.picture?.data?.url || profile.picture;
    if (pictureURL) {
      const photoId = await this.downloadAndSaveProfilePicture(
        pictureURL,
        userId,
        profile.oauth_tokens?.access_token,
      );
      if (photoId) {
        updateData.photo_id = photoId;
      }
    }

    if (profile.name && profile.name.length > (user.name?.length || 0)) {
      updateData.name = profile.name;
    }

    if (Object.keys(updateData).length > 0) {
      await this.prisma.user.update({
        where: { id: userId },
        data: updateData,
      });
    }

    await this.user.registerUserActivity(userId, 'oauth-connect');
    const { accessToken, refreshToken, session } = await this.auth.getAuthenticationPayload(locale, user.id, ipAddress, userAgent);
    await this.token.setRefreshTokenCookie(locale, res, refreshToken, session.expires_at);
    return { accessToken };
  }

  private async handleDisconnect(res: Response, locale: string, ipAddress: string, userAgent: string, provider: user_account_provider_52222e2ecb_enum, email: string) {
    const userAccount = await this.prisma.user_account.findFirst({
      where: {
        email,
        provider: String(provider).toLowerCase().replace(/-/g, '_') as user_account_provider_52222e2ecb_enum,
      },
      include: { user: true }
    });

    if (!userAccount) {
      throw new NotFoundException(`Usuário não conectado ao ${provider}.`);
    }

    await this.prisma.user_account.delete({
      where: { id: userAccount.id },
    });

    await this.user.registerUserActivity(userAccount.user.id, 'oauth-disconnect');
    const { accessToken, refreshToken, session } = await this.auth.getAuthenticationPayload(locale, userAccount.user.id, ipAddress, userAgent);
    await this.token.setRefreshTokenCookie(locale, res, refreshToken, session.expires_at);
    return { accessToken };
  }

  private async downloadAndSaveProfilePicture(
    url: string,
    userId: number,
    accessToken?: string,
  ) {
    if (!url) return null;

    const headers: any = {};
    if (accessToken) {
      headers['Authorization'] = `Bearer ${accessToken}`;
    }

    const response = await fetch(url, { headers });
    
    if (!response.ok) {
      console.warn(`Failed to download profile picture from ${url}: ${response.statusText}`);
      return null;
    }

    const buffer = Buffer.from(await response.arrayBuffer());

    const file = {
      originalname: `profile_${userId}.jpg`,
      mimetype: response.headers.get('content-type') || 'image/jpeg',
      buffer,
      size: buffer.length,
    } as any;

    const savedFile = await this.file.upload(USER_AVATAR_UPLOAD_DESTINATION, file);
    return savedFile.id;
  }
}
