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

import {
  type useObjectFileSystem, FileNotFoundObjectFileSystemError,
} from '../objectfilesystem.js';
import type { ResourceStore } from './resourcestore';
import { DataConstraintApiError, ForeignKeyDataConstraintApiError } from '../apierror';


type ResourceHourInput = {
  readonly resource: string,
  readonly startTime: DateTime, readonly endTime: DateTime,
};
export type ResourceHourInDB = ResourceHourInput;

interface FillResourceHourStoreMutationOp {
  opcode: 'fill';
  param: { resource: string, startTime: DateTime, endTime: DateTime };
}
interface UnfillResourceHourStoreMutationOp {
  opcode: 'unfill';
  param: { resource: string, startTime: DateTime, endTime: DateTime };
}
export type ResourceHourStoreMutationOp = (
  FillResourceHourStoreMutationOp | UnfillResourceHourStoreMutationOp
);

export type ResourceHourStoreQueryParam = {
  readonly resource: string,
  readonly startTime: DateTime, readonly endTime: DateTime,
};


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

  let resourceHours: ResourceHourInDB[] = [];

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

  function checkResourceHourConstraint(resourceHour: ResourceHourInDB) {
    if(cmp(resourceHour.startTime, resourceHour.endTime) >= 0) {
      throw new DataConstraintApiError('nagative_duration', resourceHour);
    }
    if(!resourceStore.resources.map(
      x => x.id
    ).includes(resourceHour.resource)) {
      throw new ForeignKeyDataConstraintApiError(
        'resources', resourceHour.resource
      );
    }
  }

  async function mutate(ops: readonly ResourceHourStoreMutationOp[]) {
    try {
      for(let op of ops) {
        if(op.opcode === 'fill') {
          const param = op.param;
          checkResourceHourConstraint(param);

          const items = resourceHours.map(
            (x, i) => [i, x] as const
          ).filter(([i, x]) => (
            x.resource === param.resource &&
            cmp(param.startTime, x.endTime) <= 0 &&
            cmp(x.startTime, param.endTime) <= 0
          )).sort((a, b) => cmp(a[1].startTime, b[1].startTime));

          let newStartTime, newEndTime;
          if(items.length) {
            newStartTime = cmp(
              items[0][1].startTime, param.startTime
            ) < 0 ? items[0][1].startTime : param.startTime;
            newEndTime = cmp(
              items[items.length - 1][1].endTime, param.endTime
            ) < 0 ? param.endTime : items[items.length - 1][1].endTime;
          } else {
            newStartTime = param.startTime;
            newEndTime = param.endTime;
          }

          for(const [i, x] of items.reverse()) {
            resourceHours.splice(i, 1);
          }

          resourceHours.push({
            resource: param.resource, startTime: newStartTime, endTime: newEndTime,
          });
        } else if(op.opcode === 'unfill') {
          const param = op.param;
          checkResourceHourConstraint(param);

          const items = resourceHours.map(
            (x, i) => [i, x] as const
          ).filter(([i, x]) => (
            x.resource === param.resource &&
            cmp(param.startTime, x.endTime) < 0 &&
            cmp(x.startTime, param.endTime) < 0
          )).sort((a, b) => cmp(a[1].startTime, b[1].startTime));

          if(items.length) {
            for(const [i, x] of items.reverse()) {
              resourceHours.splice(i, 1);
            }
            if(cmp(items[0][1].startTime, param.startTime) < 0) {
              resourceHours.push({
                resource: param.resource,
                startTime: items[0][1].startTime, endTime: param.startTime,
              });
            }
            if(cmp(param.endTime, items[items.length - 1][1].endTime) < 0) {
              resourceHours.push({
                resource: param.resource, startTime: param.endTime,
                endTime: items[items.length - 1][1].endTime,
              });
            }
          }
        } else {
          throw new Error('Unrecognized op');
        }
      }
      await objectFileSystem.put(filePath, { resourceHours });
    } catch(e) {
      await loadFromFile();
      throw e;
    }
  }

  async function query(param: ResourceHourStoreQueryParam) {
    const items = resourceHours.filter(x => (
      x.resource === param.resource &&
      cmp(param.startTime, x.endTime) < 0 &&
      cmp(x.startTime, param.endTime) < 0
    )).sort((a, b) => cmp(a.startTime, b.startTime));
    if(items.length === 0) return [];
    if(cmp(items[0].startTime, param.startTime) < 0) {
      items[0] = { ...items[0], startTime: param.startTime };
    }
    if(cmp(param.endTime, items[items.length - 1].endTime) < 0) {
      items[items.length - 1] = {
        ...items[items.length - 1], endTime: param.endTime,
      };
    }
    return items.map(x => ({
      startTime: x.startTime, endTime: x.endTime,
    }));
  }

  async function export_() {
    return { resourceHours };
  }

  async function import_(
    value: { resourceHours: ResourceHourInDB[] }
  ) {
    await objectFileSystem.put(filePath, value);
    resourceHours.splice(0, resourceHours.length, ...value.resourceHours);
  }

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

export type ResourceHourStore = Awaited<
  ReturnType<typeof useResourceHourStore>
>;
