'use server';

import { revalidatePath } from 'next/cache';
import { redirect, unstable_rethrow } from 'next/navigation';
import { verifySession } from '@/lib/auth/dal';
import { BackendRequestError } from '@/lib/http/backend-fetch';
import { fetchSchoolGradeCodes } from '@/lib/data/repositories/common-code-repository';
import {
  uploadAttachment,
  uploadAttachments,
} from '@/lib/data/repositories/file-repository';
import {
  buildComicPayload,
  buildShortsPayload,
  createContent,
  deleteContent,
  updateContent,
} from '@/lib/data/repositories/content-repository';
import {
  createQuiz,
  deleteQuiz,
  updateQuiz,
} from '@/lib/data/repositories/content-quiz-repository';
import { CONTENT_TYPE_CODE, type ContentKind } from '@/lib/domain/content';
import {
  ContentInputError,
  validateComicForm,
  validateQuizForm,
  validateShortsForm,
  type ContentFormState,
} from '@/lib/domain/content-form';
import { contentListPath } from '@/lib/domain/content-query';
import {
  FILE_MODULE,
  MAX_ATTACHMENT_BYTES,
  allowedExtensions,
  isAllowedFileExtension,
  type FileModule,
} from '@/lib/domain/file-module';

/**
 * 콘텐츠관리 Server Action — 세 종류가 같은 파일을 쓴다(폼 구조가 거의 같다).
 *
 * 이슈: 등록·수정·삭제 API가 백엔드에 아직 없다. Repository가 예상 규약대로 호출하고 여기서
 *   실패 사유를 그대로 화면에 돌려준다.
 */

const SAVE_FAILED_MESSAGE = '저장하지 못했습니다. 잠시 후 다시 시도해 주세요.';
const DELETE_FAILED_MESSAGE = '삭제하지 못했습니다. 잠시 후 다시 시도해 주세요.';
const INVALID_REQUEST_MESSAGE = '요청이 올바르지 않습니다.';
const VIDEO_FAILED_MESSAGE =
  '영상을 올리지 못했습니다. 파일을 지우거나 다시 골라 주세요 — 이대로 저장하면 영상 없이 저장됩니다.';
const VIDEO_UPLOADING_MESSAGE =
  '영상을 올리는 중입니다. 업로드가 끝난 뒤 저장해 주세요.';
const OVERSIZE_MESSAGE = `첨부파일은 ${Math.floor(MAX_ATTACHMENT_BYTES / (1024 * 1024))}MB까지 올릴 수 있습니다.`;

function readString(formData: FormData, key: string): string {
  const value = formData.get(key);
  return typeof value === 'string' ? value : '';
}

function readFiles(formData: FormData, key: string): Array<File | null> {
  return formData
    .getAll(key)
    .map((entry) =>
      entry instanceof File && entry.size > 0 ? entry : null
    );
}

function readGrades(formData: FormData): string[] {
  return readString(formData, 'grades')
    .split(',')
    .map((grade) => grade.trim())
    .filter(Boolean);
}

function readKeywords(formData: FormData) {
  const names = formData.getAll('keyword');
  const descriptions = formData.getAll('keywordDescription');
  return names.map((name, index) => ({
    keyword: typeof name === 'string' ? name : '',
    description:
      typeof descriptions[index] === 'string' ? (descriptions[index] as string) : '',
  }));
}

/**
 * 새로 고른 파일을 알맞은 저장소에 올려 파일 ID를 얻는다. 고르지 않았으면 폼이 들고 있던 기존
 * ID를 그대로 쓴다. 확장자는 백엔드가 사유 없이 실패하기 전에 여기서 먼저 잡는다.
 */
function assertUploadable(file: File, module: FileModule): void {
  if (file.size > MAX_ATTACHMENT_BYTES) {
    throw new ContentInputError(OVERSIZE_MESSAGE);
  }
  if (!isAllowedFileExtension(module, file.name)) {
    throw new ContentInputError(
      `${module.description}에는 ${allowedExtensions(module).join(', ')} 파일만 올릴 수 있습니다.`
    );
  }
}

async function resolveFileId(
  file: File | null,
  retainedId: string,
  module: FileModule
): Promise<string> {
  if (!file) {
    return retainedId;
  }
  assertUploadable(file, module);
  return uploadAttachment(file, module.id);
}

/**
 * 쓰기 동작 한 건을 감싼다 — 여기서 새는 예외가 하나라도 있으면 Next가 직렬화하지 못하고
 * `Cannot read properties of undefined (reading 'stack')`로 끝난다. 코드표 조회·업로드·저장이
 * 모두 이 안에서 일어나야 한다. `redirect()`는 예외로 흐름을 끊으므로 반드시 밖에서 부른다.
 */
async function runWrite(
  run: () => Promise<ContentFormState | null>
): Promise<ContentFormState | null> {
  try {
    return await run();
  } catch (error) {
    // 이슈: 세션이 밀렸을 때 backendFetch가 던지는 redirect를 이 catch가 삼키면 안 된다.
    unstable_rethrow(error);
    // 진짜 원인은 여기에만 남는다 — 화면에는 사람이 고칠 수 있는 사유만 나간다.
    console.error('[contents] 저장 실패', error);
    return failure(error, SAVE_FAILED_MESSAGE);
  }
}

/**
 * 사유를 화면에 낼지 정한다. 백엔드가 돌려준 메시지와 입력 오류만 그대로 보이고, 나머지는
 * 개발자용 문구(예: TypeError)라 일반 문구로 바꾼다 — 그대로 내면 사용자가 할 수 있는 일이 없다.
 */
function failure(error: unknown, fallback: string): ContentFormState {
  const showable =
    error instanceof ContentInputError || error instanceof BackendRequestError;
  return {
    status: 'error',
    message: showable && error.message ? error.message : fallback,
  };
}

function readShortsValues(formData: FormData) {
  return {
    schoolCode: readString(formData, 'schoolCode'),
    grades: readGrades(formData),
    title: readString(formData, 'title'),
    subject: readString(formData, 'subject'),
    keywords: readKeywords(formData),
    videoFileId: readString(formData, 'videoFileId'),
    thumbnailFileId: readString(formData, 'thumbnailFileId'),
    isVisible: readString(formData, 'isVisible') === 'true',
  };
}

function readComicValues(formData: FormData) {
  return {
    schoolCode: readString(formData, 'schoolCode'),
    grades: readGrades(formData),
    name: readString(formData, 'name'),
    subject: readString(formData, 'subject'),
    keywords: readKeywords(formData),
    message: readString(formData, 'message'),
    thumbnailFileId: '',
    cutFileIds: [] as string[],
    isVisible: readString(formData, 'isVisible') === 'true',
  };
}

function readQuizValues(formData: FormData) {
  return {
    schoolCode: readString(formData, 'schoolCode'),
    grades: readGrades(formData),
    points: readString(formData, 'quizPoint'),
    level: readString(formData, 'level'),
    keywords: readKeywords(formData),
    question: readString(formData, 'question'),
    answer: readString(formData, 'answer'),
    answerMessageCode: readString(formData, 'answerMessageCode'),
    answerMessageText: readString(formData, 'answerMessageText'),
    explanation: readString(formData, 'explanation'),
    isVisible: readString(formData, 'isVisible') === 'true',
  };
}

async function resolveThumbnailId(formData: FormData): Promise<string> {
  return resolveFileId(
    readFiles(formData, 'thumbnailFile')[0] ?? null,
    readString(formData, 'thumbnailFileId'),
    FILE_MODULE.contentThumbnail
  );
}

/**
 * 컷은 **한 첨부 아래** 올린다 — 백엔드가 `fileSn`을 1,2,3…으로 채번한다. 그래서 돌려주는
 * 배열은 컷 수만큼 같은 아이디가 반복된다(컷을 가리키는 건 순번이지 아이디가 아니다).
 *
 * 한 컷만 바꿔도 묶음 전체를 다시 올려야 순번이 맞으므로, 새로 고른 것이 하나라도 있으면
 * 고른 것들로 묶음을 새로 만든다. 하나도 고르지 않았으면 기존 아이디를 그대로 쓴다.
 */
async function resolveCutFileIds(formData: FormData): Promise<string[]> {
  // 고르지 않은 칸은 null로 오므로 걸러 낸다.
  const files = readFiles(formData, 'cutFile').filter(
    (file): file is File => file !== null
  );
  const retained = formData
    .getAll('cutFileId')
    .map((entry) => (typeof entry === 'string' ? entry : ''))
    .filter(Boolean);

  if (files.length === 0) {
    return retained;
  }

  for (const file of files) {
    assertUploadable(file, FILE_MODULE.toonContent);
  }

  const attachmentId = await uploadAttachments(files, FILE_MODULE.toonContent.id);
  return files.map(() => attachmentId);
}

export async function createShortsAction(
  _prevState: ContentFormState,
  formData: FormData
): Promise<ContentFormState> {
  await verifySession();

  if (readString(formData, 'videoUploading') === '1') {
    return { status: 'error', message: VIDEO_UPLOADING_MESSAGE };
  }
  if (readString(formData, 'videoUploadFailed') === '1') {
    return { status: 'error', message: VIDEO_FAILED_MESSAGE };
  }

  const failed = await runWrite(async () => {
    const validation = validateShortsForm(
      readShortsValues(formData),
      await fetchSchoolGradeCodes()
    );
    if (!validation.ok) {
      return { status: 'error', errors: validation.errors };
    }

    await createContent(
      'shorts',
      buildShortsPayload({
        ...validation.values,
        thumbnailFileId: await resolveThumbnailId(formData),
      }),
      CONTENT_TYPE_CODE.shorts
    );
    return null;
  });
  if (failed) {
    return failed;
  }

  revalidatePath(contentListPath('shorts'));
  redirect(contentListPath('shorts'));
}

export async function updateShortsAction(
  _prevState: ContentFormState,
  formData: FormData
): Promise<ContentFormState> {
  await verifySession();

  const cntntsSn = readString(formData, 'cntntsSn');
  if (!cntntsSn) {
    return { status: 'error', message: INVALID_REQUEST_MESSAGE };
  }
  if (readString(formData, 'videoUploading') === '1') {
    return { status: 'error', message: VIDEO_UPLOADING_MESSAGE };
  }
  if (readString(formData, 'videoUploadFailed') === '1') {
    return { status: 'error', message: VIDEO_FAILED_MESSAGE };
  }

  const failed = await runWrite(async () => {
    const validation = validateShortsForm(
      readShortsValues(formData),
      await fetchSchoolGradeCodes()
    );
    if (!validation.ok) {
      return { status: 'error', errors: validation.errors };
    }

    await updateContent(
      'shorts',
      cntntsSn,
      buildShortsPayload({
        ...validation.values,
        thumbnailFileId: await resolveThumbnailId(formData),
      })
    );
    return null;
  });
  if (failed) {
    return failed;
  }

  revalidatePath(contentListPath('shorts'));
  redirect(contentListPath('shorts'));
}

export async function createComicAction(
  _prevState: ContentFormState,
  formData: FormData
): Promise<ContentFormState> {
  await verifySession();

  const failed = await runWrite(async () => {
    const validation = validateComicForm(
      {
        ...readComicValues(formData),
        thumbnailFileId: await resolveThumbnailId(formData),
        cutFileIds: await resolveCutFileIds(formData),
      },
      await fetchSchoolGradeCodes()
    );
    if (!validation.ok) {
      return { status: 'error', errors: validation.errors };
    }

    await createContent(
      'comic',
      buildComicPayload(validation.values),
      CONTENT_TYPE_CODE.comic
    );
    return null;
  });
  if (failed) {
    return failed;
  }

  revalidatePath(contentListPath('comic'));
  redirect(contentListPath('comic'));
}

export async function updateComicAction(
  _prevState: ContentFormState,
  formData: FormData
): Promise<ContentFormState> {
  await verifySession();

  const cntntsSn = readString(formData, 'cntntsSn');
  if (!cntntsSn) {
    return { status: 'error', message: INVALID_REQUEST_MESSAGE };
  }

  const failed = await runWrite(async () => {
    const validation = validateComicForm(
      {
        ...readComicValues(formData),
        thumbnailFileId: await resolveThumbnailId(formData),
        cutFileIds: await resolveCutFileIds(formData),
      },
      await fetchSchoolGradeCodes()
    );
    if (!validation.ok) {
      return { status: 'error', errors: validation.errors };
    }

    await updateContent('comic', cntntsSn, buildComicPayload(validation.values));
    return null;
  });
  if (failed) {
    return failed;
  }

  revalidatePath(contentListPath('comic'));
  redirect(contentListPath('comic'));
}

export async function createQuizAction(
  _prevState: ContentFormState,
  formData: FormData
): Promise<ContentFormState> {
  await verifySession();

  const failed = await runWrite(async () => {
    const validation = validateQuizForm(
      readQuizValues(formData),
      await fetchSchoolGradeCodes()
    );
    if (!validation.ok) {
      return { status: 'error', errors: validation.errors };
    }

    await createQuiz(validation.values);
    return null;
  });
  if (failed) {
    return failed;
  }

  revalidatePath(contentListPath('quiz'));
  redirect(contentListPath('quiz'));
}

export async function updateQuizAction(
  _prevState: ContentFormState,
  formData: FormData
): Promise<ContentFormState> {
  await verifySession();

  const quizSn = readString(formData, 'quizSn');
  if (!quizSn) {
    return { status: 'error', message: INVALID_REQUEST_MESSAGE };
  }

  const failed = await runWrite(async () => {
    const validation = validateQuizForm(
      readQuizValues(formData),
      await fetchSchoolGradeCodes()
    );
    if (!validation.ok) {
      return { status: 'error', errors: validation.errors };
    }

    await updateQuiz(quizSn, validation.values);
    return null;
  });
  if (failed) {
    return failed;
  }

  revalidatePath(contentListPath('quiz'));
  redirect(contentListPath('quiz'));
}

export async function deleteContentAction(
  kind: ContentKind,
  id: string
): Promise<ContentFormState> {
  await verifySession();

  if (!id) {
    return { status: 'error', message: INVALID_REQUEST_MESSAGE };
  }

  try {
    if (kind === 'quiz') {
      await deleteQuiz(id);
    } else {
      await deleteContent(kind, id);
    }
  } catch (error) {
    unstable_rethrow(error);
    return failure(error, DELETE_FAILED_MESSAGE);
  }

  revalidatePath(contentListPath(kind));
  return { status: 'success' };
}
