import { markRaw } from 'vue';
import { DateTime, cmp } from '@silane/datetime';

import {
  deleteUndefinedProperties, arrayRemoveIf,
} from '../../common/utils.js';
import { getObjectID } from '../../common/zkan-utils.js';
import {
  type useObjectFileSystem, FileNotFoundObjectFileSystemError,
} from '../objectfilesystem.js';
import type { TimeLabelStore } from './timelabelstore';
import type { ResourceStore } from './resourcestore';
import {
  DataConstraintApiError, ForeignKeyDataConstraintApiError,
  UniqueDataConstraintApiError,
} from '../apierror.js';


type Mutable<T> = { -readonly [P in keyof T]: T[P] };
type Jsonable<T> = { [P in keyof T]: T[P] extends DateTime ? string : T[P] };

interface EventId {
  readonly id: string;
}
interface EventTmpId {
  readonly __id: string;
}
interface EventInputBase {
  readonly type: string;
  readonly resources: readonly string[];
  readonly startTime: DateTime;
  readonly endTime: DateTime;
  readonly timeLabel: string | null;
  readonly memo: string;
  readonly color: string;
}
interface StandardEventInput extends EventInputBase {
  readonly type: 'standard';
}
interface LessonEventInput extends EventInputBase {
  readonly type: 'lesson';
  readonly subject: string;
  readonly frame: string;
}
export type EventInput = StandardEventInput | LessonEventInput;

interface EventChangeId {
  readonly fromEvent: EventId | EventTmpId;
}
interface EventChangeInput extends EventChangeId {
  readonly toEvent: EventId | EventTmpId | null;
  readonly causeResources: readonly string[];
}


interface EventStoreMutationOpBase {
  readonly opcode: string;
  readonly param: any;
}

export interface AddEventEventStoreMutationOp extends EventStoreMutationOpBase {
  opcode: 'add_event';
  param: EventInput | (EventInput & EventTmpId);
}
export interface UpdateEventEventStoreMutationOp extends EventStoreMutationOpBase {
  opcode: 'update_event';
  param: EventInput & EventId | EventInput & EventTmpId;
}
export interface RemoveEventEventStoreMutationOp extends EventStoreMutationOpBase {
  opcode: 'remove_event';
  param: EventId | EventTmpId;
}
export interface AddEventChangeEventStoreMutationOp extends EventStoreMutationOpBase {
  opcode: 'add_eventchange';
  param: EventChangeInput;
}
export interface RemoveEventChangeEventStoreMutationOp extends EventStoreMutationOpBase {
  opcode: 'remove_eventchange';
  param: EventChangeId;
}
export interface ClearEventStoreMutationOp extends EventStoreMutationOpBase {
  opcode: 'clear';
  param: null;
}

export type EventStoreMutationOp = (
  AddEventEventStoreMutationOp | UpdateEventEventStoreMutationOp
  | RemoveEventEventStoreMutationOp
  | AddEventChangeEventStoreMutationOp | RemoveEventChangeEventStoreMutationOp
  | ClearEventStoreMutationOp
);


export type StandardEvent = EventId & StandardEventInput;
export type LessonEvent = EventId & LessonEventInput;
export type EventInDB = StandardEvent | LessonEvent;
export interface EventChangeInDB {
  fromEvent: string;
  toEvent: string | null;
  causeResources: readonly string[];
}

export type EventStoreQueryParam = {
  readonly id?: string | null,
  readonly startTime?: DateTime, readonly endTime?: DateTime,
};


function setIntersection<T>(setA: Set<T>, setB: Set<T>) {
  return new Set([...setA].filter(x => setB.has(x)));
}
function setDiff<T>(setA: Set<T>, setB: Set<T>) {
  return new Set([...setA].filter(x => !setB.has(x)));
}

function isEventId(
  eventIdOrEventTmpId: any
): eventIdOrEventTmpId is EventId {
  return eventIdOrEventTmpId.id != null;
}

export async function useEventStore(
  objectFileSystem: ReturnType<typeof useObjectFileSystem>,
  timeLabelStore: TimeLabelStore, resourceStore: ResourceStore,
) {
  const filePath = '/event.json';

  let events: EventInDB[] = [];
  let eventChanges: EventChangeInDB[] = [];

  async function loadFromFile() {
    try {
      ({ events, eventChanges } = await objectFileSystem.get(filePath));
    } catch(e) {
      if(!(e instanceof FileNotFoundObjectFileSystemError)) {
        throw e;
      }
      events.splice(0, events.length);
      eventChanges.splice(0, eventChanges.length);
    }
  }
  await loadFromFile();

  function checkEventConstraint(event: EventInDB) {
    if(cmp(event.startTime, event.endTime) >= 0) {
      throw new DataConstraintApiError('nagative_duration', event);
    }
    const resourceIds = new Set(
      resourceStore.resources.map(x => x.id)
    );
    if(!event.resources.every(x => resourceIds.has(x))) {
      throw new ForeignKeyDataConstraintApiError('resources', '');
    }
    if(event.timeLabel != null && timeLabelStore.timeLabels.every(
      x => x.id !== event.timeLabel
    )) {
      throw new ForeignKeyDataConstraintApiError(
        'timeLabels', event.timeLabel
      );
    }
  }

  function checkEventChangeConstraint(eventChange: EventChangeInDB) {
    const eventIds = new Set(events.map(x => x.id));
    if(!eventIds.has(eventChange.fromEvent)) {
      throw new ForeignKeyDataConstraintApiError(
        'events', eventChange.fromEvent
      );
    }
    if(eventChange.toEvent != null && !eventIds.has(
      eventChange.toEvent
    )) {
      throw new ForeignKeyDataConstraintApiError(
        'events', eventChange.toEvent
      );
    }
    const resourceIds = new Set(
      resourceStore.resources.map(x => x.id)
    );
    if(!eventChange.causeResources.every(x => resourceIds.has(x))) {
      throw new ForeignKeyDataConstraintApiError('resources', '');
    }
  }

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

    try {
      for(const [opIdx, op] of ops.map((x, i) => [i, x] as const)) {
        if(op.opcode === 'add_event') {
          const param = [op.param];
          const ids = [getObjectID()];
          for(let [idx, e] of param.entries()) {
            if('__id' in e && e.__id != null) {
                eventTmpIdMap.set(e.__id, ids[idx]);
            }
          }
          const newEvents: EventInDB[] = param.map((x, idx) => ({
            id: ids[idx], resources: x.resources,
            startTime: markRaw(x.startTime), endTime: markRaw(x.endTime),
            timeLabel: x.timeLabel, memo: x.memo, color: x.color,
            ... x.type === 'lesson' ? {
              type: 'lesson', subject: x.subject, frame: x.frame,
            } : { type: 'standard' },
          }));
          for(let e of newEvents) {
            checkEventConstraint(e);
          }
          events.push(...newEvents);
        } else if(op.opcode === 'update_event') {
          const param = [op.param];
          const trueIds = param.map(
            x => isEventId(x) ? x.id : eventTmpIdMap.get(x.__id)
          );
          for(let [id, x] of param.map((x, i) => [trueIds[i], x] as const)) {
            const eventIdx = events.findIndex(e => e.id === id);
            if(eventIdx >= 0) {
              const newEvent: EventInDB = deleteUndefinedProperties({
                ...events[eventIdx], resources: x.resources,
                startTime: markRaw(x.startTime), endTime: markRaw(x.endTime),
                timeLabel: x.timeLabel, memo: x.memo, color: x.color,
                ... x.type === 'lesson' ? {
                  type: 'lesson', subject: x.subject, frame: x.frame,
                } : { type: 'standard', subject: undefined, frame: undefined },
              });
              checkEventConstraint(newEvent);
              events[eventIdx] = newEvent;
            }
          }
        } else if(op.opcode === 'remove_event') {
          const param = [op.param];
          const trueIds = new Set(param.map(
            x => isEventId(x) ? x.id : eventTmpIdMap.get(x.__id)
          ));
          arrayRemoveIf(eventChanges, x => trueIds.has(x.fromEvent) || (
            x.toEvent != null && trueIds.has(x.toEvent)
          ));
          for(let idx = 0; idx < events.length; ++idx) {
            if(trueIds.delete(events[idx].id)) {
              events.splice(idx, 1);
              --idx;
            }
          }
        } else if(op.opcode === 'add_eventchange') {
          const param = [op.param];
          const trueEventIds = param.map(x => ({ fromEvent: (
            isEventId(x.fromEvent)
            ? x.fromEvent.id : eventTmpIdMap.get(x.fromEvent.__id)!
          ), toEvent: x.toEvent && (
            isEventId(x.toEvent)
            ? x.toEvent.id : eventTmpIdMap.get(x.toEvent.__id)!
          ) }));
          if(trueEventIds.some(
            x => x.fromEvent === undefined || x.toEvent === undefined
          )) {
            throw new ForeignKeyDataConstraintApiError(
              'events', ''
            );
          }
          const newEventChanges = param.map((x, i) => ({
            ...trueEventIds[i], causeResources: x.causeResources,
          }));
          for(let ec of newEventChanges) {
            checkEventChangeConstraint(ec);
          }
          if(setIntersection(
            new Set(eventChanges.map(x => x.fromEvent)),
            new Set(newEventChanges.map(x => x.fromEvent))
          ).size) {
            throw new UniqueDataConstraintApiError([
              opIdx, 'param', 'fromEvent', 'id',
            ]);
          }
          if(setDiff(setIntersection(
            new Set(eventChanges.map(x => x.toEvent)),
            new Set(newEventChanges.map(x => x.toEvent))
          ), new Set([null])).size) {
            throw new UniqueDataConstraintApiError([
              opIdx, 'param', 'toEvent', 'id',
            ]);
          }
          eventChanges.push(...newEventChanges);
        } else if(op.opcode === 'remove_eventchange') {
          const param = [op.param];
          const trueFromEventIds = new Set(param.map(x => (
            isEventId(x.fromEvent)
            ? x.fromEvent.id : eventTmpIdMap.get(x.fromEvent.__id)
          )));
          arrayRemoveIf(
            eventChanges, x => trueFromEventIds.has(x.fromEvent)
          );
        } else if(op.opcode === 'clear') {
          events.splice(0);
          eventChanges.splice(0);
        } else {
          throw new Error('Unrecognized op');
        }
      }
      await objectFileSystem.put(filePath, { events, eventChanges });
    } catch(e) {
      await loadFromFile();
      throw e;
    }
  }

  async function query(param: EventStoreQueryParam) {
    const queue = new Set(events.filter(x => (
      param.id == null || x.id === param.id
    ) && (
      !param.startTime || cmp(param.startTime, x.endTime) < 0
    ) && (
      !param.endTime || cmp(x.startTime, param.endTime) < 0
    )).map(x => x.id));
    const filteredEvents = new Set<string>();
    const filteredEventChanges = new Set<EventChangeInDB>();
    while(queue.size) {
      const eventID = queue.keys().next().value;
      queue.delete(eventID);
      filteredEvents.add(eventID);
      let eventChange = eventChanges.find(x => x.fromEvent === eventID);
      if(eventChange) {
        filteredEventChanges.add(eventChange);
        if(eventChange.toEvent && !filteredEvents.has(eventChange.toEvent)) {
          queue.add(eventChange.toEvent);
        }
      }
      eventChange = eventChanges.find(x => x.toEvent === eventID);
      if(eventChange) {
        filteredEventChanges.add(eventChange);
        if(!filteredEvents.has(eventChange.fromEvent)) {
          queue.add(eventChange.fromEvent);
        }
      }
    }
    return {
      events: [...filteredEvents].map(x => events.find(e => e.id === x)!),
      eventChanges: [...filteredEventChanges],
    };
  }

  async function export_() {
    return { events, eventChanges };
  }

  async function import_(
    value: { events: EventInDB[], eventChanges: EventChangeInDB[] }
  ) {
    await objectFileSystem.put(filePath, value);
    events.splice(0, events.length, ...value.events);
    eventChanges.splice(0, eventChanges.length, ...value.eventChanges);
  }

  return {
    mutate, query, export: export_, import: import_,
  };
}

export type EventStore = Awaited<ReturnType<typeof useEventStore>>;
