import { ActionError, defineAction } from 'astro:actions';
import { Session } from '@util/session.ts';
import { Permissions } from '@util/permissions.ts';
import { db } from '@db/database.ts';
import { z } from 'astro:schema';
import { MAX_UPLOAD_BYTES, UPLOAD_PATH } from 'astro:env/server';
import fs from 'node:fs';
import crypto from 'node:crypto';
import path from 'node:path';
import { sendWebhook, WebhookAction } from '@util/webhook.ts';
import { allowedImageTypes, allowedVideoTypes } from '@util/media.ts';

export const report = {
  submitReport: defineAction({
    input: z.object({
      urlHash: z.string(),
      reported: z.string().nullish(),
      reason: z.string(),
      body: z.string(),
      files: z
        .array(
          z
            .instanceof(File)
            .refine((f) => [...allowedImageTypes, ...allowedVideoTypes].findIndex((v) => v === f.type) !== -1)
        )
        .nullable()
    }),
    handler: async (input) => {
      const fileSize = input.files?.reduce((prev, curr) => prev + curr.size, 0);
      if (fileSize && fileSize > MAX_UPLOAD_BYTES) {
        throw new ActionError({
          code: 'BAD_REQUEST',
          message: 'Die Anhänge sind zu groß'
        });
      }

      const report = await db.getReportByUrlHash({ urlHash: input.urlHash });
      if (!report) {
        throw new ActionError({
          code: 'NOT_FOUND'
        });
      }

      let reportedId = report.reported?.id ?? null;
      if (input.reported != report.reported?.username) {
        if (input.reported == null) reportedId = null;
        else {
          const reportedUser = await db.getUserByUsername({ username: input.reported });
          if (!reportedUser)
            throw new ActionError({
              code: 'NOT_FOUND'
            });
          reportedId = reportedUser.id;
        }
      }

      if (!UPLOAD_PATH) {
        throw new ActionError({
          code: 'FORBIDDEN',
          message: 'Es dürfen keine Anhänge hochgeladen werden'
        });
      }

      const filePaths = [] as string[];
      try {
        await db.transaction(async (tx) => {
          for (const file of input.files ?? []) {
            const uuid = crypto.randomUUID();
            const tmpFilePath = path.join(UPLOAD_PATH!, uuid);
            const tmpFileStream = fs.createWriteStream(tmpFilePath);

            filePaths.push(tmpFilePath);

            const md5Hash = crypto.createHash('md5');

            for await (const chunk of file.stream()) {
              md5Hash.update(chunk);
              tmpFileStream.write(chunk);
            }

            const hash = md5Hash.digest('hex');
            const filePath = path.join(UPLOAD_PATH!, hash);

            let type: 'image' | 'video';
            if (allowedImageTypes.includes(file.type)) {
              type = 'image';
            } else if (allowedVideoTypes.includes(file.type)) {
              type = 'video';
            } else {
              throw new ActionError({
                code: 'BAD_REQUEST',
                message: 'Invalid file type'
              });
            }

            await tx.addReportAttachment({
              type: type,
              hash: hash,
              reportId: report.id
            });

            fs.renameSync(tmpFilePath, filePath);
            filePaths.pop();
            filePaths.push(filePath);
          }

          await tx.submitReport({
            urlHash: input.urlHash,
            reportedId: reportedId,
            reason: input.reason,
            body: input.body
          });
        });
      } catch (e) {
        for (const filePath of filePaths) {
          fs.rmSync(filePath);
        }

        throw e;
      }

      sendWebhook(WebhookAction.Report, {
        reporter: report.reporter.uuid,
        reported: report.reported?.uuid ?? null,
        reason: input.reason
      });
    },
    accept: 'form'
  }),
  addReport: defineAction({
    input: z.object({
      reason: z.string(),
      body: z.string().nullable(),
      createdAt: z.string().datetime().nullable(),
      reporter: z.number(),
      reported: z.number().nullable()
    }),
    handler: async (input, context) => {
      Session.actionSessionFromCookies(context.cookies, Permissions.Reports);

      const { id } = await db.addReport({
        reason: input.reason,
        body: input.body,
        createdAt: input.createdAt ? new Date(input.createdAt) : null,
        reporterId: input.reporter,
        reportedId: input.reported
      });

      return {
        id: id
      };
    }
  }),
  editReport: defineAction({
    input: z.object({
      reportId: z.number(),
      reported: z.number().nullable()
    }),
    handler: async (input, context) => {
      Session.actionSessionFromCookies(context.cookies, Permissions.Reports);

      await db.editReport({
        id: input.reportId,
        reportedId: input.reported
      });
    }
  }),
  reportStatus: defineAction({
    input: z.object({
      reportId: z.number()
    }),
    handler: async (input, context) => {
      Session.actionSessionFromCookies(context.cookies, Permissions.Reports);

      return {
        reportStatus: await db.getReportStatus(input)
      };
    }
  }),
  editReportStatus: defineAction({
    input: z.object({
      reportId: z.number(),
      status: z.enum(['open', 'closed']).nullable(),
      notice: z.string().nullable(),
      statement: z.string().nullable(),
      strikeReasonId: z.number().nullable()
    }),
    handler: async (input, context) => {
      Session.actionSessionFromCookies(context.cookies, Permissions.Reports);

      const report = await db.getReportById({ id: input.reportId });
      const strike = await db.getStrikeByReportId({ reportId: input.reportId });

      let reportCreatedAt = undefined;
      if (input.status !== null && report.createdAt === null) reportCreatedAt = new Date();

      await db.transaction(async (tx) => {
        if (reportCreatedAt) await tx.editReport({ id: input.reportId, createdAt: reportCreatedAt });

        await tx.editReportStatus(input);

        if (strike?.strikeReason?.id != input.strikeReasonId) {
          if (input.strikeReasonId) {
            await db.editStrike({
              reportId: input.reportId,
              strikeReasonId: input.strikeReasonId
            });
          } else {
            await db.deleteStrike({ reportId: input.reportId });
          }
        }
      });

      if (input.status === 'closed' && strike?.strikeReason?.id != input.strikeReasonId) {
        if (report.reported) {
          const user = await db.getUserById({ id: report.reported.id });

          // send webhook in background
          sendWebhook(WebhookAction.Strike, {
            uuid: user!.uuid!
          });
        }
      }
    }
  }),
  reports: defineAction({
    input: z.object({
      reporter: z.string().nullish(),
      reported: z.string().nullish(),
      includeDrafts: z.boolean().nullish()
    }),
    handler: async (input, context) => {
      Session.actionSessionFromCookies(context.cookies, Permissions.Reports);

      return {
        reports: await db.getReports(input)
      };
    }
  }),
  reportAttachments: defineAction({
    input: z.object({
      reportId: z.number()
    }),
    handler: async (input, context) => {
      Session.actionSessionFromCookies(context.cookies, Permissions.Reports);

      return {
        reportAttachments: (await db.getReportAttachments(input)) ?? []
      };
    }
  }),
  addStrikeReason: defineAction({
    input: z.object({
      name: z.string(),
      weight: z.number()
    }),
    handler: async (input, context) => {
      Session.actionSessionFromCookies(context.cookies, Permissions.Admin);

      return await db.addStrikeReason(input);
    }
  }),
  editStrikeReason: defineAction({
    input: z.object({
      id: z.number(),
      name: z.string(),
      weight: z.number()
    }),
    handler: async (input, context) => {
      Session.actionSessionFromCookies(context.cookies, Permissions.Admin);

      await db.editStrikeReason(input);
    }
  }),
  deleteStrikeReason: defineAction({
    input: z.object({
      id: z.number()
    }),
    handler: async (input, context) => {
      Session.actionSessionFromCookies(context.cookies, Permissions.Admin);

      await db.deleteStrikeReason(input);
    }
  }),
  strikeReasons: defineAction({
    handler: async (_, context) => {
      Session.actionSessionFromCookies(context.cookies, Permissions.Reports);

      return {
        strikeReasons: await db.getStrikeReasons({})
      };
    }
  }),
  usernames: defineAction({
    input: z.object({
      username: z.string()
    }),
    handler: async (input) => {
      const users = await db.getUsers({ username: input.username, limit: 5 });

      return {
        usernames: users.map((u) => u.username)
      };
    }
  })
};
