import { Body, Controller, Delete, Get, Headers, Param, Post, Query, Res, UnauthorizedException, UsePipes, ValidationPipe } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ApiBearerAuth, ApiBody, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
import { firstValueFrom } from 'rxjs';
import { CoreGrpcService } from '../core-grpc.service';
import { OAuthAuthorizeIncomingDto, OAuthConsentActionDto, OAuthTokenIncomingDto } from '../dto/oauth.dto';
import { extractBearerToken } from '../auth-token';
import { appendQueryParams, mapOAuthClientPublicInfo, mapOAuthConsentCheckResponse, mapUserInfoToOidc, mapUserOAuthConsentsResponse, mergeTokenCredentials, mapTokenResponseToStandard, normalizeAuthorizeQuery, normalizeConsentQuery, normalizeTokenBody } from '../lib/oauth-params';
import { resolveFrontendUrl } from '../lib/oauth-issuer';
import { verifyAccessToken } from '../session-auth';

type HttpResponse = {
  redirect(status: number, url: string): void;
};

const oauthValidationPipe = new ValidationPipe({
  whitelist: true,
  transform: true,
  forbidNonWhitelisted: false
});

@ApiTags('OAuth 2.0')
@Controller('oauth')
export class OAuthController {
  constructor(
    private readonly core: CoreGrpcService,
    private readonly jwt: JwtService
  ) {}

  @Get('authorize')
  @UsePipes(oauthValidationPipe)
  @ApiOperation({
    summary: 'OAuth / OIDC авторизация',
    description:
      'Поддерживает стандартные query-параметры RFC 6749 / OIDC (client_id, redirect_uri, response_type, code_challenge) и legacy camelCase. ' +
      'Без userId и без Bearer-токена перенаправляет на страницу подтверждения доступа.'
  })
  @ApiQuery({ name: 'client_id', required: false, description: 'OAuth client_id (RFC 6749)' })
  @ApiQuery({ name: 'clientId', required: false, description: 'OAuth client_id (legacy)' })
  @ApiQuery({ name: 'redirect_uri', required: false })
  @ApiQuery({ name: 'redirectUri', required: false })
  @ApiQuery({ name: 'scope', required: false })
  @ApiQuery({ name: 'state', required: false })
  @ApiQuery({ name: 'response_type', required: false, example: 'code' })
  @ApiQuery({ name: 'code_challenge', required: false })
  @ApiQuery({ name: 'code_challenge_method', required: false })
  @ApiQuery({ name: 'userId', required: false, description: 'Legacy: ID пользователя после входа' })
  @ApiResponse({ status: 302, description: 'Redirect на redirect_uri с authorization code' })
  async authorize(
    @Query() query: OAuthAuthorizeIncomingDto,
    @Headers('authorization') authorization: string | undefined,
    @Headers('accept') acceptHeader: string | undefined,
    @Res({ passthrough: true }) res: HttpResponse
  ) {
    const normalized = normalizeAuthorizeQuery(query as Record<string, unknown>);
    let userId: string | undefined;

    if (authorization) {
      try {
        const payload = await verifyAccessToken(this.jwt, authorization);
        userId = payload.sub;
      } catch {
        // токен недействителен — не доверяем userId из query
      }
    }

    if (!userId) {
      const frontendUrl = await resolveFrontendUrl(this.core);
      const consentQuery = { ...(query as Record<string, unknown>) };
      delete consentQuery.userId;
      delete consentQuery.user_id;
      const consentUrl = appendQueryParams(`${frontendUrl}/auth/oauth/authorize`, consentQuery);
      res.redirect(302, consentUrl);
      return;
    }

    const consentCheck = (await firstValueFrom(
      this.core.oauth.CheckOAuthConsent({
        userId,
        clientId: normalized.clientId,
        scope: normalized.scope
      })
    )) as { granted?: boolean };

    const accept = acceptHeader ?? '';
    const wantsJson = authorization?.startsWith('Bearer') || accept.includes('application/json');

    if (!consentCheck.granted) {
      if (wantsJson) {
        const pendingConsent = (await firstValueFrom(
          this.core.oauth.CheckOAuthConsent({
            userId,
            clientId: normalized.clientId,
            scope: normalized.scope
          })
        )) as Record<string, unknown>;
        return mapOAuthConsentCheckResponse(pendingConsent);
      }
      const frontendUrl = await resolveFrontendUrl(this.core);
      const consentQuery = { ...(query as Record<string, unknown>) };
      delete consentQuery.userId;
      delete consentQuery.user_id;
      const consentUrl = appendQueryParams(`${frontendUrl}/auth/oauth/authorize`, consentQuery);
      res.redirect(302, consentUrl);
      return;
    }

    const result = (await firstValueFrom(
      this.core.oauth.Authorize({
        userId,
        clientId: normalized.clientId,
        redirectUri: normalized.redirectUri,
        scope: normalized.scope,
        state: normalized.state,
        nonce: normalized.nonce
      })
    )) as { redirectUrl?: string; code?: string; state?: string };

    if (wantsJson) {
      return result;
    }

    if (!result.redirectUrl) {
      throw new UnauthorizedException('Не удалось создать authorization code');
    }

    res.redirect(302, result.redirectUrl);
  }

  @Get('consent/check')
  @ApiBearerAuth()
  @UsePipes(oauthValidationPipe)
  @ApiOperation({ summary: 'Проверить сохранённое согласие OAuth', description: 'Возвращает статус согласия пользователя для указанного OAuth-приложения и scopes.' })
  @ApiQuery({ name: 'client_id', required: false })
  @ApiQuery({ name: 'clientId', required: false })
  @ApiQuery({ name: 'scope', required: false })
  async checkConsent(
    @Query() query: OAuthConsentActionDto,
    @Headers('authorization') authorization: string | undefined
  ) {
    const payload = await verifyAccessToken(this.jwt, authorization);
    const normalized = normalizeConsentQuery(query as Record<string, unknown>);
    const result = (await firstValueFrom(
      this.core.oauth.CheckOAuthConsent({
        userId: payload.sub,
        clientId: normalized.clientId,
        scope: normalized.scope
      })
    )) as Record<string, unknown>;
    return mapOAuthConsentCheckResponse(result);
  }

  @Get('clients/:clientId/public')
  @ApiOperation({
    summary: 'Публичная информация об OAuth-приложении',
    description: 'Возвращает человекочитаемое название приложения для экрана согласия.'
  })
  async getClientPublicInfo(@Param('clientId') clientId: string) {
    const result = (await firstValueFrom(
      this.core.oauth.GetOAuthClientPublicInfo({ clientId })
    )) as Record<string, unknown>;
    return mapOAuthClientPublicInfo(result);
  }

  @Post('consent/approve')
  @ApiBearerAuth()
  @UsePipes(oauthValidationPipe)
  @ApiOperation({
    summary: 'Подтвердить OAuth-согласие',
    description: 'Сохраняет согласие пользователя и выдаёт authorization code для redirect_uri.'
  })
  @ApiBody({ type: OAuthConsentActionDto })
  async approveConsent(
    @Body() body: OAuthConsentActionDto,
    @Headers('authorization') authorization: string | undefined
  ) {
    const payload = await verifyAccessToken(this.jwt, authorization);
    const normalized = normalizeAuthorizeQuery(body as Record<string, unknown>);
    return firstValueFrom(
      this.core.oauth.Authorize({
        userId: payload.sub,
        clientId: normalized.clientId,
        redirectUri: normalized.redirectUri,
        scope: normalized.scope,
        state: normalized.state,
        nonce: normalized.nonce,
        grantConsent: true
      })
    );
  }

  @Get('consents/users/:userId')
  @ApiBearerAuth()
  @ApiOperation({ summary: 'Список OAuth-согласий пользователя', description: 'Возвращает все приложения, которым пользователь выдал доступ к данным.' })
  async listUserConsents(
    @Param('userId') userId: string,
    @Headers('authorization') authorization: string | undefined
  ) {
    const payload = await verifyAccessToken(this.jwt, authorization);
    if (payload.sub !== userId) {
      throw new UnauthorizedException('Можно просматривать только свои согласия');
    }
    const result = (await firstValueFrom(this.core.oauth.ListUserOAuthConsents({ userId }))) as Record<string, unknown>;
    return mapUserOAuthConsentsResponse(result);
  }

  @Delete('consents/users/:userId/:consentId')
  @ApiBearerAuth()
  @ApiOperation({ summary: 'Отозвать OAuth-согласие', description: 'Удаляет сохранённое согласие. При следующем входе приложение снова запросит доступ.' })
  async revokeConsent(
    @Param('userId') userId: string,
    @Param('consentId') consentId: string,
    @Headers('authorization') authorization: string | undefined
  ) {
    const payload = await verifyAccessToken(this.jwt, authorization);
    if (payload.sub !== userId) {
      throw new UnauthorizedException('Можно отзывать только свои согласия');
    }
    return firstValueFrom(this.core.oauth.RevokeOAuthConsent({ userId, consentId }));
  }

  @Post('token')
  @UsePipes(oauthValidationPipe)
  @ApiOperation({
    summary: 'Выдать OAuth токены',
    description:
      'Поддерживает grant_type=authorization_code и grant_type=refresh_token. ' +
      'Принимает application/x-www-form-urlencoded (RFC 6749) и JSON. ' +
      'Ответ — snake_case: access_token, token_type, expires_in, refresh_token, id_token.'
  })
  @ApiBody({ type: OAuthTokenIncomingDto })
  @ApiResponse({ status: 201, description: 'OAuth токены выданы' })
  async token(@Body() body: OAuthTokenIncomingDto, @Headers('authorization') authorization?: string) {
    const normalized = mergeTokenCredentials(normalizeTokenBody(body as Record<string, unknown>), authorization);
    const result = (await firstValueFrom(
      this.core.oauth.Token({
        grantType: normalized.grantType,
        code: normalized.code,
        refreshToken: normalized.refreshToken,
        clientId: normalized.clientId,
        clientSecret: normalized.clientSecret,
        redirectUri: normalized.redirectUri
      })
    )) as {
      accessToken?: string;
      tokenType?: string;
      expiresIn?: number;
      refreshToken?: string;
      idToken?: string;
    };
    return mapTokenResponseToStandard(result);
  }

  @Get('userinfo')
  @ApiBearerAuth()
  @ApiOperation({ summary: 'OAuth userinfo', description: 'Возвращает профиль пользователя по OAuth access token.' })
  @ApiResponse({ status: 200, description: 'Профиль пользователя получен' })
  async userInfo(@Headers('authorization') authorization?: string) {
    const result = (await firstValueFrom(
      this.core.oauth.UserInfo({ accessToken: extractBearerToken(authorization) })
    )) as {
      sub?: string;
      email?: string;
      phone?: string;
      name?: string;
      picture?: string;
      emailVerified?: boolean;
      preferredUsername?: string;
      phoneNumber?: string;
      phoneNumberVerified?: boolean;
    };
    return mapUserInfoToOidc(result);
  }
}
