import {
  ExecutionContext,
  CallHandler,
  NestInterceptor,
  Injectable,
  createParamDecorator,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { ApiFieldError } from '../common/error/api-error';
import { ApiErrorCodes } from '../common/error/api-error-codes';
import { Connection } from 'typeorm';
import { InjectDataSource } from '@nestjs/typeorm';
import { DECORATORS } from '@nestjs/swagger/dist/constants';
import { decodeBigNumberKey, isValidBigNumberKey } from './big-number-key';
import { AssetRights } from '../asset/logic/Rights';
import { getProjectLicense } from '../license/get-project-license';
import { ProjectLicense } from '../asset/logic/ProjectLicense';
import { SYSTEM_USER_ID } from '../common/dto/user-dto';

export enum AccessTag {
  ROOT = 'root',
  TASKS = 'tasks',
}

@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();
    //headers
    const userId = req.user ? req.user.id : null;

    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,
    );

    // req.projectUser.accessTags.add(AccessTag.ROOT);

    return handler.handle();
  }
}

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

  const project_q = connection
    .createQueryBuilder('projects', 'p')
    .where('p.id = :project_id', {
      project_id: decodeBigNumberKey(projectId),
    });
  if (!userId) {
    project_q.andWhere(
      '(p.is_public_gdd OR p.is_public_tasks OR p.is_public_about OR p.is_public_pulse)',
    );
  }
  const project = await project_q
    .select([
      'p.is_public_gdd',
      'p.is_public_tasks',
      'p.is_public_about',
      'p.is_public_pulse',
    ])
    .getRawOne();
  if (!project) {
    throw new ApiFieldError(
      'Project not found',
      ApiErrorCodes.ENTITY_NOT_FOUND,
      'pid',
    );
  }
  const project_access_params: ProjectAccessParams = {
    isPublicAbout: project.is_public_about,
    isPublicGdd: project.is_public_gdd,
    isPublicPulse: project.is_public_pulse,
    isPublicTasks: project.is_public_tasks,
  };

  let project_license: ProjectLicense | null = null;
  let role:
    | {
        num: number;
        is_admin: boolean;
        title: string;
        default_asset_rights: AssetRights;
        default_workspace_rights: AssetRights;
      }
    | undefined = undefined;
  if (userId) {
    role = 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 AS is_admin',
        'pr.title as title',
        'pr.num AS num',
        'pr.default_asset_rights AS default_asset_rights',
        'pr.default_workspace_rights AS default_workspace_rights',
      ])
      .getRawOne();
    if (role) {
      project_license = await getProjectLicense(connection, projectId);
    }
  }

  const accessTags = new Set<AccessTag>();
  if (userId === SYSTEM_USER_ID) accessTags.add(AccessTag.ROOT);

  return {
    projectId: projectId,
    projectParams: project_access_params,
    userId: userId,
    userRole: role
      ? {
          isAdmin: role.is_admin,
          num: role.num,
          title: role.title,
          defaultAssetRights: role.default_asset_rights,
          defaultWorkspaceRights: role.default_workspace_rights,
        }
      : null,
    accessTags,
    projectLicense: project_license,
  };
}

export type ProjectRole = {
  num: number;
  title: string;
  isAdmin: boolean;
  defaultAssetRights: AssetRights;
  defaultWorkspaceRights: AssetRights;
};

export type ProjectAccessParams = {
  isPublicGdd: boolean;
  isPublicTasks: boolean;
  isPublicAbout: boolean;
  isPublicPulse: boolean;
};

export type ProjectAccess = {
  projectId: string;
  projectParams: ProjectAccessParams;
  userId: number | null;
  userRole: ProjectRole | null;
  accessTags: Set<AccessTag>;
  projectLicense: ProjectLicense | null;
};

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],
      );
    },
  ],
);
