import * as plugins from '../../plugins.js';
import { SimpleLocalClient } from '../../core/index.js';
import { LocalCalendarMapper } from './local_calendar.mapper.js';
import type { ILocalCalendarConfig, ILocalCalendarEvent, ILocalCalendarSnapshot } from './local_calendar.types.js';
import { localCalendarProfile } from './local_calendar.types.js';

export class LocalCalendarClient extends SimpleLocalClient<ILocalCalendarConfig> {
  constructor(private readonly configArg: ILocalCalendarConfig) {
    super(localCalendarProfile, configArg);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<ILocalCalendarSnapshot> {
    if (this.shouldReadLocalCalendar()) {
      try {
        return LocalCalendarMapper.toSnapshot({ config: this.configArg, rawData: await this.readLocalCalendar(), online: true, source: 'manual' });
      } catch (errorArg) {
        return LocalCalendarMapper.toSnapshot({ config: this.configArg, online: false, source: 'runtime', error: errorMessage(errorArg) });
      }
    }

    const snapshot = await super.getSnapshot(forceRefreshArg);
    if (snapshot.rawData === undefined && snapshot.entities.length) {
      return LocalCalendarMapper.toSnapshot({ config: { ...this.configArg, snapshot }, online: snapshot.online, source: snapshot.source, error: snapshot.error });
    }

    return LocalCalendarMapper.toSnapshot({ config: this.configArg, rawData: snapshot.rawData, online: snapshot.online, source: snapshot.source, error: snapshot.error });
  }

  private shouldReadLocalCalendar(): boolean {
    return Boolean(this.filePath() && !this.configArg.snapshot && this.configArg.rawData === undefined && this.configArg.rawText === undefined && !this.configArg.entities?.length && !this.configArg.client && !this.configArg.snapshotProvider && !this.configArg.host);
  }

  private async readLocalCalendar(): Promise<Record<string, unknown>> {
    const filePath = this.filePath();
    if (!filePath) {
      throw new Error('Local Calendar snapshots require config.filePath, config.file_path, config.icsFile, or config.ics_file.');
    }

    const ics = await readTextOrEmpty(filePath);
    const events = parseIcsEvents(ics);
    return {
      filePath,
      ics_file: filePath,
      calendarName: this.calendarName() || localCalendarProfile.defaultName,
      contentBytes: Buffer.byteLength(ics, 'utf8'),
      eventCount: events.length,
      nextEvent: nextEvent(events),
      events,
    };
  }

  private filePath(): string | undefined {
    const metadata = this.configArg.metadata;
    return stringValue(this.configArg.filePath) || stringValue(this.configArg.file_path) || stringValue(this.configArg.icsFile) || stringValue(this.configArg.ics_file) || stringValue(metadata?.filePath) || stringValue(metadata?.file_path) || stringValue(metadata?.icsFile) || stringValue(metadata?.ics_file);
  }

  private calendarName(): string | undefined {
    const metadata = this.configArg.metadata;
    return stringValue(this.configArg.calendarName) || stringValue(this.configArg.calendar_name) || stringValue(metadata?.calendarName) || stringValue(metadata?.calendar_name) || this.configArg.name;
  }
}

const readTextOrEmpty = async (filePathArg: string): Promise<string> => {
  try {
    return await plugins.fs.readFile(filePathArg, 'utf8');
  } catch (errorArg) {
    if (isNodeError(errorArg) && errorArg.code === 'ENOENT') {
      return '';
    }
    throw errorArg;
  }
};

const parseIcsEvents = (icsArg: string): ILocalCalendarEvent[] => {
  const events: ILocalCalendarEvent[] = [];
  let current: Record<string, string> | undefined;

  for (const line of unfoldIcsLines(icsArg)) {
    if (line.toUpperCase() === 'BEGIN:VEVENT') {
      current = {};
      continue;
    }
    if (line.toUpperCase() === 'END:VEVENT') {
      if (current) {
        events.push(eventFromProperties(current));
      }
      current = undefined;
      continue;
    }
    if (!current) {
      continue;
    }

    const colonIndex = line.indexOf(':');
    if (colonIndex === -1) {
      continue;
    }
    const propertyName = line.slice(0, colonIndex).split(';')[0].toUpperCase();
    current[propertyName] = decodeIcsText(line.slice(colonIndex + 1));
  }

  return events.sort((leftArg, rightArg) => eventTime(leftArg) - eventTime(rightArg));
};

const unfoldIcsLines = (icsArg: string): string[] => {
  const unfolded: string[] = [];
  for (const line of icsArg.split(/\r?\n/)) {
    if ((line.startsWith(' ') || line.startsWith('\t')) && unfolded.length) {
      unfolded[unfolded.length - 1] += line.slice(1);
      continue;
    }
    unfolded.push(line.trimEnd());
  }
  return unfolded;
};

const eventFromProperties = (propertiesArg: Record<string, string>): ILocalCalendarEvent => {
  const start = parseIcsDate(propertiesArg.DTSTART);
  const end = parseIcsDate(propertiesArg.DTEND);
  return {
    uid: propertiesArg.UID,
    summary: propertiesArg.SUMMARY || '',
    description: propertiesArg.DESCRIPTION,
    location: propertiesArg.LOCATION,
    start,
    end,
    startRaw: propertiesArg.DTSTART,
    endRaw: propertiesArg.DTEND,
    rrule: propertiesArg.RRULE,
    allDay: Boolean(propertiesArg.DTSTART && /^\d{8}$/.test(propertiesArg.DTSTART)),
  };
};

const parseIcsDate = (valueArg: string | undefined): string | undefined => {
  if (!valueArg) {
    return undefined;
  }
  const dateMatch = /^(\d{4})(\d{2})(\d{2})$/.exec(valueArg);
  if (dateMatch) {
    return `${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}`;
  }
  const dateTimeMatch = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z?)$/.exec(valueArg);
  if (!dateTimeMatch) {
    return valueArg;
  }
  const value = `${dateTimeMatch[1]}-${dateTimeMatch[2]}-${dateTimeMatch[3]}T${dateTimeMatch[4]}:${dateTimeMatch[5]}:${dateTimeMatch[6]}${dateTimeMatch[7] ? 'Z' : ''}`;
  return dateTimeMatch[7] ? new Date(value).toISOString() : value;
};

const nextEvent = (eventsArg: ILocalCalendarEvent[]): ILocalCalendarEvent | undefined => {
  const now = Date.now();
  return eventsArg.find((eventArg) => eventTime(eventArg) >= now) || eventsArg[0];
};

const eventTime = (eventArg: ILocalCalendarEvent): number => {
  const timestamp = Date.parse(eventArg.start || eventArg.startRaw || '');
  return Number.isFinite(timestamp) ? timestamp : Number.MAX_SAFE_INTEGER;
};

const decodeIcsText = (valueArg: string): string => valueArg.replace(/\\n/gi, '\n').replace(/\\,/g, ',').replace(/\\;/g, ';').replace(/\\\\/g, '\\');

const isNodeError = (valueArg: unknown): valueArg is NodeJS.ErrnoException => valueArg instanceof Error && 'code' in valueArg;

const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;

const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
