import {
  ExecutionContext,
  CallHandler,
  NestInterceptor,
  Injectable,
  createParamDecorator,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { ApiError, ApiFieldError } from '../common/error/api-error';
import { ApiErrorCodes } from '../common/error/api-error-codes';
import { Connection, Repository } from 'typeorm';
import { InjectDataSource } from '@nestjs/typeorm';
import { DECORATORS } from '@nestjs/swagger/dist/constants';
import { IProjectInfo } from 'src/project/types/types';
import { decodeBigNumberKey, isValidBigNumberKey } from './big-number-key';

@Injectable()
export class ProjectAccessInterceptor implements NestInterceptor {
  constructor(@InjectDataSource() private readonly connection: Connection) {}

  async intercept(
    context: ExecutionContext,
    handler: CallHandler,
  ): Promise<Observable<any>> {
    const req = context.switchToHttp().getRequest();

    const userId = req.user ? req.user.id : null;
    if (!userId) {
      throw new ApiError('User is not authorized', ApiErrorCodes.ACCESS_DENIED);
    }

    const projectId = req.query.pid;
    if (!projectId) {
      throw new ApiFieldError(
        'Project id is not set',
        ApiErrorCodes.PARAM_EMPTY,
        'pid',
      );
    }

    req.projectUser = await checkProjectAccess(
      this.connection,
      userId,
      projectId,
    );

    return handler.handle();
  }
}

export async function checkProjectAccess(
  connection: Connection,
  userId: number,
  projectId: string,
): Promise<ProjectAccess> {
  if (!isValidBigNumberKey(projectId)) {
    throw new ApiFieldError(
      'Project id is not valid',
      ApiErrorCodes.PARAM_BAD_FORMAT,
      'pid',
    );
  }

  const record = await connection
    .createQueryBuilder()
    .from('project_users', 'pu')
    .innerJoin(
      'project_roles',
      'pr',
      'pu.project_id = pr.project_id AND pu.user_role_num = pr.num',
    )
    .where('pu.project_id = :projectId', {
      projectId: decodeBigNumberKey(projectId),
    })
    .andWhere('pu.user_id = :userId', { userId })
    .andWhere('pu.left_at IS NULL')
    .select(['pr.is_admin', 'pr.title'])
    .getRawOne();
  if (!record) {
    throw new ApiError(
      'User has no access to project ' + projectId,
      ApiErrorCodes.ACCESS_DENIED,
    );
  }

  return {
    projectId: projectId,
    userId: userId,
    userRole: {
      isAdmin: record.is_admin,
      title: record.title,
    },
  };
}

export async function getProjectInfo(
  projectRepo: Repository<any>,
  projectUserRepo: Repository<any>,
  projectId: string,
): Promise<IProjectInfo> {
  const project = await projectRepo.findOne({
    id: projectId,
  });
  if (!project) {
    throw new ApiError(
      'Project with id =' + projectId + "doesn't exist",
      ApiErrorCodes.ACCESS_DENIED,
    );
  }

  const members = await getMembers(projectUserRepo, projectId);

  return {
    id: project.id,
    title: project.title,
    createdAt: project.createdAt,
    updatedAt: project.updatedAt,
    members,
  };
}

export async function getMembers(
  projectUserRepo: Repository<any>,
  projectId: string,
) {
  const members = await projectUserRepo
    .createQueryBuilder('pu')
    .leftJoin(`users`, 'u', 'pu.user_id = u.id')
    .where('pu.project_id = :id_project AND pu.left_at IS NULL', {
      id_project: decodeBigNumberKey(projectId),
    })
    .select([
      'u.id as id',
      'u.name as name',
      'u.email as email',
      'pu.user_role_num as role',
    ])
    .getRawMany();

  return members;
}

export type ProjectRole = {
  title: string;
  isAdmin: boolean;
};

export type ProjectAccess = {
  projectId: string;
  userId: number;
  userRole: ProjectRole;
};

export const RequestProjectAccess = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    if (!request.projectUser) {
      throw new Error(
        'ProjectIdInterceptor interceptor was not called before RequestProjectAccess',
      );
    }
    return request.projectUser;
  },
  [
    (target, key, index) => {
      const explicit =
        Reflect.getMetadata(DECORATORS.API_PARAMETERS, target[key]) ?? [];
      Reflect.defineMetadata(
        DECORATORS.API_PARAMETERS,
        [
          ...explicit,
          {
            description: 'Project id',
            in: 'query',
            name: 'pid',
            required: true,
            type: 'string',
          },
        ],
        target[key],
      );
    },
  ],
);
