import { readonly, shallowReactive } from 'vue';

import { FileNotFoundObjectFileSystemError, type useObjectFileSystem } from '../objectfilesystem';
import { arrayRemoveIf, comparerByKey } from '../../common/utils';
import { getObjectID } from '../../common/zkan-utils';
import { ForeignKeyDataConstraintApiError } from '../apierror.js';


export type StudentGrade = {
  id: string, label: string,
};
export type StudentGradeInDB = StudentGrade;

type BaseResourceInput = { multiplicity: number, disabled: boolean };
type ClassroomResourceInput = BaseResourceInput & {
  multiplicity: -1, type: 'classroom', label: string,
};
type TeacherResourceInput = BaseResourceInput & {
  multiplicity: 1, type: 'teacher', firstName: string, lastName: string,
  firstNameKana: string, lastNameKana: string,
};
type StudentResourceInput = BaseResourceInput & {
  multiplicity: 1,
  type: 'student', firstName: string, lastName: string,
  firstNameKana: string, lastNameKana: string,
  grade: { id: string } | { __id: string },
};
type ResourceInput = (
  ClassroomResourceInput | TeacherResourceInput | StudentResourceInput
);

export type ClassroomResource = { id: string } & ClassroomResourceInput;
export type TeacherResource = { id: string } & TeacherResourceInput;
export type StudentResource = {
  id: string,
} & Omit<StudentResourceInput, 'grade'> & { grade: string };

export type ResourceInDB = (
  ClassroomResource | TeacherResource | StudentResource
);

export interface AddStudentGradeResourceStoreMutationOp {
  opcode: 'add_studentgrade';
  param: {
    readonly __id?: string, readonly label: string,
  };
}
export interface UpdateStudentGradeResourceStoreMutationOp {
  opcode: 'update_studentgrade';
  param: {
    readonly id: string, readonly label: string,
  };
}
export interface RemoveStudentGradeResourceStoreMutationOp {
  opcode: 'remove_studentgrade';
  param: { readonly id: string };
}
export interface AddResourceStoreMutationOp {
  opcode: 'add_resource';
  param: ResourceInput & { __id?: string };
}
export interface UpdateResourceStoreMutationOp {
  opcode: 'update_resource';
  param: ResourceInput & { id: string };
}
export interface RemoveResourceStoreMutationOp {
  opcode: 'remove_resource';
  param: { readonly id: string };
}
export interface ClearResourceStoreMutationOp {
  opcode: 'clear';
  param: unknown;
}
export type ResourceStoreMutationOp = (
  AddStudentGradeResourceStoreMutationOp |
  UpdateStudentGradeResourceStoreMutationOp |
  RemoveStudentGradeResourceStoreMutationOp |
  AddResourceStoreMutationOp | UpdateResourceStoreMutationOp |
  RemoveResourceStoreMutationOp | ClearResourceStoreMutationOp
);


export async function useResourceStore(
  objectFileSystem: ReturnType<typeof useObjectFileSystem>
) {
  const filePath = '/resource.json';

  const studentGrades = shallowReactive<StudentGradeInDB[]>([]);
  const resources = shallowReactive<ResourceInDB[]>([]);

  function sortResourcesAndStudentGrades() {
    studentGrades.sort(comparerByKey(x => x.label));
    resources.sort(comparerByKey(x => {
      if(x.type === 'classroom') return [0];
      if(x.type === 'teacher') return [1, x.lastNameKana, x.firstNameKana];
      if(x.type === 'student') return [2, studentGrades.find(
        sg => sg.id === x.grade
      )?.label, x.lastNameKana, x.firstNameKana];
    }));
  }

  async function loadFromFile() {
    let savedData;
    try {
      savedData = await objectFileSystem.get(filePath);
    } catch(e) {
      if(!(e instanceof FileNotFoundObjectFileSystemError)) {
        throw e;
      }
      savedData = { studentGrades: [], resources: [] };
    }
    studentGrades.splice(0, studentGrades.length, ...savedData.studentGrades);
    resources.splice(0, resources.length, ...savedData.resources);
    sortResourcesAndStudentGrades();
  }
  await loadFromFile();

  async function mutate(ops: readonly ResourceStoreMutationOp[]) {
    const tmpIdMap = new Map<string, string>();

    try {
      for(const op of ops) {
        if(op.opcode === 'add_studentgrade') {
          const newId = getObjectID();
          if(op.param.__id != null) {
              tmpIdMap.set(op.param.__id, newId);
          }
          studentGrades.push({
            id: newId, label: op.param.label,
          });
        } else if(op.opcode === 'update_studentgrade') {
          const index = studentGrades.findIndex(
            x => x.id === op.param.id
          );
          if(index >= 0) {
            studentGrades[index] = {
              ...studentGrades[index], label: op.param.label,
            };
          }
        } else if(op.opcode === 'remove_studentgrade') {
          const index = studentGrades.findIndex(
            x => x.id === op.param.id
          );
          if(index >= 0) {
            arrayRemoveIf(resources, x => (
              x.type === 'student' && x.grade === studentGrades[index].id
            ));
            studentGrades.splice(index, 1);
          }
        } else if(op.opcode === 'add_resource') {
          const newId = getObjectID();
          if(op.param.__id != null) {
            tmpIdMap.set(op.param.__id, newId);
          }
          if(op.param.type === 'classroom') {
            const param = op.param;
            resources.push({
              id: newId, multiplicity: -1, disabled: param.disabled,
              type: 'classroom', label: param.label,
            });
          } else if(op.param.type === 'teacher') {
            const param = op.param;
            resources.push({
              id: newId, multiplicity: 1, disabled: param.disabled,
              type: 'teacher',
              firstName: param.firstName, lastName: param.lastName,
              firstNameKana: param.firstNameKana,
              lastNameKana: param.lastNameKana,
            });
          } else if(op.param.type === 'student') {
            const param = op.param;
            const gradeId = param.grade['id'] ?? tmpIdMap.get(
              param.grade['__id']
            );
            if(!studentGrades.some(x => x.id === gradeId)) {
              throw new ForeignKeyDataConstraintApiError(
                'studentGrades', gradeId
              );
            }
            resources.push({
              id: newId, multiplicity: 1, disabled: param.disabled,
              type: 'student',
              firstName: param.firstName, lastName: param.lastName,
              firstNameKana: param.firstNameKana,
              lastNameKana: param.lastNameKana, grade: gradeId,
            });
          }
        } else if(op.opcode === 'update_resource') {
          const index = resources.findIndex(x => x.id === op.param.id);
          if(index >= 0) {
            if(op.param.type === 'classroom') {
              resources[index] = {
                id: op.param.id, multiplicity: -1, disabled: op.param.disabled,
                type: 'classroom', label: op.param.label,
              };
            } else if(op.param.type === 'teacher') {
              resources[index] = {
                id: op.param.id, multiplicity: 1, disabled: op.param.disabled,
                type: 'teacher',
                firstName: op.param.firstName, lastName: op.param.lastName,
                firstNameKana: op.param.firstNameKana,
                lastNameKana: op.param.lastNameKana,
              };
            } else if(op.param.type === 'student') {
              const gradeId = op.param.grade['id'] ?? tmpIdMap.get(
                op.param.grade['__id']
              );
              if(!studentGrades.some(x => x.id === gradeId)) {
                throw new ForeignKeyDataConstraintApiError(
                  'studentGrades', gradeId
                );
              }
              resources[index] = {
                id: op.param.id, multiplicity: 1, disabled: op.param.disabled,
                type: 'student',
                firstName: op.param.firstName, lastName: op.param.lastName,
                firstNameKana: op.param.firstNameKana,
                lastNameKana: op.param.lastNameKana, grade: gradeId,
              };
            }
          }
        } else if(op.opcode === 'remove_resource') {
          const index = resources.findIndex(x => x.id === op.param.id);
          if(index >= 0) {
            resources.splice(index, 1);
          }
        } else if(op.opcode === 'clear') {
          studentGrades.splice(0, studentGrades.length);
          resources.splice(0, resources.length);
        }
      }
      sortResourcesAndStudentGrades();
      await objectFileSystem.put(filePath, { studentGrades, resources });
      return {
        idMapping: [...tmpIdMap.entries()],
      };
    } catch(e) {
      await loadFromFile();
      throw e;
    }

  }

  async function query(param: {}) {
    return {
      studentGrades: studentGrades,
      resources: resources,
    };
  }

  async function export_() {
    return {
      studentGrades: studentGrades,
      resources: resources,
    };
  }

  async function import_(
    value: { studentGrades: StudentGradeInDB[], resources: ResourceInDB[] }
  ) {
    await objectFileSystem.put(filePath, value);
    studentGrades.splice(0, studentGrades.length, ...value.studentGrades);
    resources.splice(0, resources.length, ...value.resources);
    sortResourcesAndStudentGrades();
  }

  return {
    mutate, query, export: export_, import: import_,
    studentGrades: readonly(studentGrades), resources: readonly(resources),
  };
}

export type ResourceStore = Awaited<ReturnType<typeof useResourceStore>>;
