import {
  Body,
  Controller,
  Delete,
  Get,
  Injectable,
  Ip,
  Param,
  Patch,
  Post,
  Query,
  UseGuards,
  UseInterceptors,
  UsePipes,
  ValidationPipe,
} from '@nestjs/common';
import { AppService } from './app.service';
import {
  ApiBearerAuth,
  ApiOperation,
  ApiQuery,
  ApiTags,
  ApiOkResponse,
} from '@nestjs/swagger';
import {
  ChangeProjectSettingsDTO,
  CheckShortLinkDTO,
  CreateProjectDTO,
  PqDTO,
  ProjectInfoDTO,
  ProjectQueryDTO,
} from '../project/dto/project-dto';
import { UserDTO } from 'src/common/dto/user-dto';
import { RequestUser } from 'src/common/request-user';
import {
  ApiOkResponseListWithTotal,
  ApiResultListWithTotal,
} from 'src/common/types/api-result';
import {
  ProjectAccess,
  ProjectAccessInterceptor,
  RequestProjectAccess,
  checkProjectAccess,
} from 'src/utils/project-access';
import { InjectRepository } from '@nestjs/typeorm';
import { ProjectUserEntity } from 'src/entities/project-user.entity';
import { Repository } from 'typeorm';
import { AppLoadDTO, AppLoadProjectListItemDTO } from './dto/app-load-dto';
import { ProjectService } from '../project/project.service';
import { ApiErrorCodes } from '../common/error/api-error-codes';
import { JwtAuthGuard } from '../common/guards/jwt.auth.guard';
import { OptTokenOrJwtAuthGuard } from '../guards/opt-token-or-jwt.auth.guard';
import { GetFileTokenDTO } from './dto/get-file-token-dto';
import { parsePq } from '../project/project-query';
import { ApiError, ApiFieldError } from '../common/error/api-error';
import { GetExtAppTokenDTO } from './dto/get-ext-app-token-dto';
import { TokenOrJwtAuthGuard } from '../guards/token-or-jwt.auth.guard';
import {
  decodeBigNumberKey,
  encodeBigNumberKey,
} from '../utils/big-number-key';
import { RealIP } from 'nestjs-real-ip';

@ApiTags('app')
@Controller('app')
@UseGuards(OptTokenOrJwtAuthGuard)
@ApiBearerAuth()
@Injectable()
export class AppController {
  constructor(
    private readonly appService: AppService,
    private readonly projectService: ProjectService,
    @InjectRepository(ProjectUserEntity)
    private readonly projectUserRepo: Repository<ProjectUserEntity>,
  ) {}

  @Get('load')
  @ApiOperation({
    summary: 'Load app',
  })
  @ApiOkResponse({
    type: AppLoadDTO,
  })
  @ApiQuery({ name: 'pid', required: false })
  async loadApp(
    @RealIP() ip: string,
    @RequestUser() account: UserDTO | false,
    @Query('pid') pid?: string,
    @Query('shortLink') shortLink?: string,
  ): Promise<AppLoadDTO> {
    let projectAccess: ProjectAccess | null = null;
    if (!pid && shortLink) {
      pid = await this.appService.getProjectIdByShortLink(shortLink);
    }

    if (pid) {
      try {
        projectAccess = await checkProjectAccess(
          this.projectUserRepo.manager.connection,
          account ? account.id : null,
          pid,
        );
      } catch (err) {
        if (err.code === ApiErrorCodes.ENTITY_NOT_FOUND) {
          projectAccess = null;
        } else throw err;
      }
    }

    const res = await this.appService.appLoad(
      account ? account : null,
      projectAccess,
    );
    if (projectAccess) {
      await this.projectService.recordProjectView(
        projectAccess.projectId,
        projectAccess.userId,
        ip,
      );
    }
    return res;
  }

  @Get('projects')
  @UseGuards(TokenOrJwtAuthGuard)
  @ApiOperation({
    summary: 'Get project list',
  })
  @ApiOkResponseListWithTotal(AppLoadProjectListItemDTO)
  @UsePipes(
    new ValidationPipe({
      transform: true,
      transformOptions: { enableImplicitConversion: true },
    }),
  )
  async getProjects(
    @RequestUser() user: UserDTO,
    @Query() filterData: ProjectQueryDTO,
  ): Promise<ApiResultListWithTotal<AppLoadProjectListItemDTO>> {
    return await this.appService.getProjectList(user, filterData);
  }

  @Post('projects')
  @UseGuards(TokenOrJwtAuthGuard)
  @ApiOperation({
    summary: 'Create project',
  })
  @ApiOkResponse({
    type: ProjectInfoDTO,
  })
  async createProject(
    @RequestUser() account: UserDTO,
    @Body() params: CreateProjectDTO,
  ): Promise<ProjectInfoDTO> {
    return await this.appService.createProject(account, params);
  }

  @Patch('projects/:id')
  @UseGuards(TokenOrJwtAuthGuard)
  @ApiOperation({
    summary: 'Change project',
  })
  @ApiOkResponse({
    type: ProjectInfoDTO,
  })
  async changeProject(
    @RequestUser() account: UserDTO,
    @Param('id') id: string,
    @Body() params: ChangeProjectSettingsDTO,
  ): Promise<ProjectInfoDTO> {
    const projectAccess = await checkProjectAccess(
      this.projectUserRepo.manager.connection,
      account.id,
      id,
    );
    return await this.appService.changeProject(projectAccess, params);
  }

  @Delete('projects/:id')
  @UseGuards(TokenOrJwtAuthGuard)
  @ApiOperation({
    summary: 'Delete project',
  })
  async deleteProject(
    @RequestUser() account: UserDTO,
    @Param('id') id: string,
  ): Promise<void> {
    const projectAccess = await checkProjectAccess(
      this.projectUserRepo.manager.connection,
      account.id,
      id,
    );
    await this.appService.deleteProject(projectAccess);
  }

  @Post('check')
  @UseGuards(TokenOrJwtAuthGuard)
  @ApiOperation({
    summary: 'Check short link',
  })
  @UsePipes(
    new ValidationPipe({
      transform: true,
      transformOptions: { enableImplicitConversion: true },
    }),
  )
  @UseInterceptors(ProjectAccessInterceptor)
  async checkShortLink(
    @Body() params: CheckShortLinkDTO,
    @RequestProjectAccess() projectAccess: ProjectAccess,
  ): Promise<boolean> {
    return await this.appService.checkShortLink(params.link, projectAccess);
  }

  @Get('ext/token')
  @ApiOperation({
    summary: 'Получение токена для внешних приложений',
  })
  @ApiOkResponse({
    type: GetFileTokenDTO,
  })
  async getExtAppToken(
    @Query() params: GetExtAppTokenDTO,
    @RequestUser() account?: UserDTO,
  ): Promise<GetFileTokenDTO> {
    if (!account) {
      throw new ApiError('Need auth', ApiErrorCodes.ACCESS_DENIED);
    }
    const license_features = params.licenseFeatures
      ? JSON.parse(params.licenseFeatures)
      : [];
    if (!Array.isArray(license_features)) {
      throw new ApiFieldError(
        'Wrong licenseFeatures',
        ApiErrorCodes.PARAM_BAD_VALUE,
        'licenseFeatures',
      );
    } else if (license_features.some((l) => typeof l !== 'string')) {
      throw new ApiFieldError(
        'Wrong licenseFeatures',
        ApiErrorCodes.PARAM_BAD_VALUE,
        'licenseFeatures',
      );
    }
    const ids = await this.projectService.getProjectIdsByProjectQuery(
      {
        ...parsePq(params.pq),
        my: true,
      },
      account.id,
    );
    return {
      token: await this.appService.getExtAppToken(
        ids,
        account,
        license_features,
      ),
    };
  }

  @Get('tool/encode-big-number/:val')
  @ApiOperation({
    summary: 'Encode big number key',
  })
  async toolEncodeBigNumber(@Param('val') val: string): Promise<string> {
    return encodeBigNumberKey(val);
  }

  @Get('tool/decode-big-number/:val')
  @ApiOperation({
    summary: 'Decode big number key',
  })
  async toolDecodeBigNumber(@Param('val') val: string): Promise<string> {
    return decodeBigNumberKey(val).toString();
  }
}
