import type {FastifyInstance, FastifyReply, FastifyRequest} from 'fastify';
import {ZodTypeProvider} from 'fastify-type-provider-zod';
import {strictObject, string, z} from 'zod';

import config from '../../../config';
import {
  authAuthenticationRequestSchema,
  authAuthenticationRequiredSchema,
  type AuthAuthenticationResponse,
  authAuthenticationResponseSchema,
  authRegistrationResponseSchema,
  authRegistrationSchema,
  authUpdateUserSchema,
  type AuthVerificationPreloadConfigs,
  authVerificationPreloadConfigsSchema,
  type AuthVerificationResponse,
  authVerificationResponseSchema,
} from '../../../shared/schema/api/auth';
import {type Credentials, credentialsSchema} from '../../../shared/schema/Auth';
import {AccessLevel} from '../../../shared/schema/constants/Auth';
import {NotFoundError, UnauthorizedError} from '../../errors';
import {
  authenticateHook,
  getRequiredAuthContext,
  resolveRequestUser,
  setAuthContext,
} from '../../middleware/authenticate';
import requireAdmin from '../../middleware/requireAdmin';
import Users from '../../models/Users';
import {bootstrapServicesForUser, destroyUserServices, getAllServices} from '../../services';
import {clearAuthCookie, getAuthToken, setAuthCookie} from '../../util/authUtil';
import {rateLimit} from '../utils';

const failedLoginResponse = 'Failed login.';

const sendAuthenticationResponse = (
  reply: FastifyReply,
  credentials: Required<Pick<Credentials, 'username' | 'level'>>,
): void => {
  const {username, level} = credentials;

  setAuthCookie(reply, getAuthToken(username));

  const response: AuthAuthenticationResponse = {
    success: true,
    username,
    level,
  };

  reply.send(response);
};

const preloadConfigs: AuthVerificationPreloadConfigs = {
  authMethod: config.authMethod,
  pollInterval: config.torrentClientPollInterval,
};

const usernameParamSchema = credentialsSchema.pick({username: true});

const registrationQuerySchema = strictObject({cookie: string().optional()});

const authRoutes = async (fastify: FastifyInstance) => {
  const authRateLimitOptions = rateLimit({
    windowMs: 5 * 60 * 1000,
    max: 200,
  });

  const typedFastify = fastify.withTypeProvider<ZodTypeProvider>();

  typedFastify.post(
    '/authenticate',
    {
      schema: {
        summary: 'Authenticate user',
        description: 'Authenticate a user and set auth cookie.',
        tags: ['Auth'],
        body: authAuthenticationRequestSchema,
        response: {
          200: authAuthenticationResponseSchema,
          400: z
            .object({
              message: z.string(),
            })
            .strict(),
          401: z
            .object({
              message: z.string(),
            })
            .strict(),
        },
      },
      ...(authRateLimitOptions ?? {}),
    },
    async (req, reply): Promise<void> => {
      if (config.authMethod === 'none') {
        sendAuthenticationResponse(reply, Users.getConfigUser());
        return;
      }

      const parsedCredentials = authAuthenticationRequiredSchema.safeParse(req.body);

      if (!parsedCredentials.success) {
        reply.status(400).send({
          message: 'Missing username or password.',
        });
        return;
      }

      const credentials = parsedCredentials.data;

      try {
        const level = await Users.comparePassword(credentials);

        sendAuthenticationResponse(reply, {username: credentials.username, level});
      } catch {
        reply.status(401).send({
          message: failedLoginResponse,
        });
      }
    },
  );

  const ensureRegistrationPermission = async (req: FastifyRequest, reply: FastifyReply) => {
    await Users.initialUserGate({
      handleInitialUser: () => undefined,
      handleSubsequentUser: async () => {
        const user = await resolveRequestUser(req);
        if (user == null) {
          throw new UnauthorizedError();
        }
        setAuthContext(req, {user, services: getAllServices(user)!});
        if (reply.sent) {
          return;
        }

        await requireAdmin(req, reply);
      },
    });
  };

  typedFastify.post(
    '/register',
    {
      ...(authRateLimitOptions ?? {}),
      preHandler: ensureRegistrationPermission,
      schema: {
        summary: 'Register user',
        description: 'Register a new user. Optionally skip setting auth cookie.',
        tags: ['Auth'],
        body: authRegistrationSchema,
        querystring: registrationQuerySchema,
        response: {
          200: z.union([authRegistrationResponseSchema, authAuthenticationResponseSchema]),
          404: z.string(),
        },
      },
    },
    async (req, reply) => {
      if (config.authMethod === 'none') {
        reply.status(404).send('Not found');
        return;
      }

      const credentials = req.body;

      const user = await Users.createUser(credentials);
      bootstrapServicesForUser(user);

      if (req.query.cookie === 'false') {
        return {username: user.username};
      }

      sendAuthenticationResponse(reply, credentials);
    },
  );

  fastify.get(
    '/verify',
    {
      ...(authRateLimitOptions ?? {}),
      schema: {
        summary: 'Verify authentication',
        description: 'Check authentication state and preload configs.',
        tags: ['Auth'],
        response: {
          200: authVerificationResponseSchema,
          401: z
            .object({
              configs: authVerificationPreloadConfigsSchema,
            })
            .strict(),
        },
      },
    },
    async (req, reply): Promise<void> => {
      if (config.authMethod === 'none') {
        const {username, level} = Users.getConfigUser();

        setAuthCookie(reply, getAuthToken(username));

        const response: AuthVerificationResponse = {
          initialUser: false,
          username,
          level,
          configs: preloadConfigs,
        };

        reply.send(response);
        return;
      }

      await Users.initialUserGate({
        handleInitialUser: () => {
          const response: AuthVerificationResponse = {
            initialUser: true,
            configs: preloadConfigs,
          };
          reply.send(response);
        },
        handleSubsequentUser: async () => {
          const user = await resolveRequestUser(req);

          if (user == null) {
            reply.status(401).send({
              configs: preloadConfigs,
            });
            return;
          }

          const response: AuthVerificationResponse = {
            initialUser: false,
            username: user.username,
            level: user.level,
            configs: preloadConfigs,
          };

          reply.send(response);
        },
      });
    },
  );

  await typedFastify.register(async (authenticatedRoutes) => {
    const typedAuthenticatedRoutes = authenticatedRoutes.withTypeProvider<ZodTypeProvider>();

    typedAuthenticatedRoutes.addHook('preHandler', authenticateHook);

    typedAuthenticatedRoutes.get(
      '/logout',
      {
        ...(authRateLimitOptions ?? {}),
        schema: {
          summary: 'Logout',
          description: 'Clear auth cookie.',
          tags: ['Auth'],
          security: [{User: []}],
          response: {
            200: z.void(),
          },
        },
      },
      (req, reply) => {
        getRequiredAuthContext(req);
        clearAuthCookie(reply);
        reply.send();
      },
    );

    await typedAuthenticatedRoutes.register(async (adminRoutes) => {
      const typedAdminRoutes = adminRoutes.withTypeProvider<ZodTypeProvider>();

      typedAdminRoutes.addHook('preHandler', requireAdmin);
      typedAdminRoutes.addHook('preHandler', async (_req, _reply) => {
        if (config.authMethod === 'none') {
          throw new NotFoundError();
        }
      });

      typedAdminRoutes.get(
        '/users',
        {
          ...(authRateLimitOptions ?? {}),
          schema: {
            summary: 'List users',
            description: 'List all users.',
            tags: ['Auth'],
            security: [{User: []}],
            response: {
              200: z.array(
                z
                  .object({
                    username: z.string(),
                    level: z.nativeEnum(AccessLevel),
                  })
                  .strict(),
              ),
            },
          },
        },
        async (_req, reply): Promise<void> => {
          const users = await Users.listUsers();
          reply.send(
            users.map((user) => ({
              username: user.username,
              level: user.level,
            })),
          );
        },
      );

      typedAdminRoutes.delete(
        '/users/:username',
        {
          ...(authRateLimitOptions ?? {}),
          schema: {
            summary: 'Delete user',
            description: 'Delete a user by username.',
            tags: ['Auth'],
            security: [{User: []}],
            params: usernameParamSchema,
            response: {
              200: z
                .object({
                  username: z.string(),
                })
                .strict(),
            },
          },
        },
        async (req, reply): Promise<void> => {
          await Users.removeUser(req.params.username);
          reply.send({username: req.params.username});
        },
      );

      typedAdminRoutes.patch(
        '/users/:username',
        {
          schema: {
            summary: 'Update user',
            description: 'Update a user by username.',
            tags: ['Auth'],
            security: [{User: []}],
            body: authUpdateUserSchema,
            params: usernameParamSchema,
            response: {
              200: z.object({}).strict(),
            },
          },
        },
        async (req) => {
          const {username} = req.params;
          const patch = req.body;

          const newUsername = await Users.updateUser(username, patch);

          const user = await Users.lookupUser(newUsername);

          await destroyUserServices(user._id);

          bootstrapServicesForUser(user);

          return {};
        },
      );
    });
  });
};

export default authRoutes;
