import * as plugins from '../../plugins.js';
import { SimpleLocalClient, type ISimpleLocalEntitySnapshot } from '../../core/index.js';
import { LocalTodoMapper } from './local_todo.mapper.js';
import type { ILocalTodoConfig, ILocalTodoSnapshot } from './local_todo.types.js';
import { localTodoProfile } from './local_todo.types.js';

export class LocalTodoClient extends SimpleLocalClient<ILocalTodoConfig> {
  constructor(private readonly configArg: ILocalTodoConfig) {
    super(localTodoProfile, configArg);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<ILocalTodoSnapshot> {
    if (this.shouldReadLocalTodoFile()) {
      try {
        return this.snapshotFromIcs(await this.readLocalTodoFile());
      } catch (errorArg) {
        return LocalTodoMapper.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 LocalTodoMapper.toSnapshot({ config: { ...this.configArg, snapshot }, online: snapshot.online, source: snapshot.source, error: snapshot.error });
    }

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

  private shouldReadLocalTodoFile(): boolean {
    return Boolean(this.todoFilePath() && !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 readLocalTodoFile(): Promise<{ filePath: string; content: string }> {
    const filePath = this.todoFilePath();
    if (!filePath) {
      throw new Error('Local To-do snapshots require config.filePath, config.icsPath, config.storagePath, or a local config.path.');
    }

    try {
      return { filePath, content: await plugins.fs.readFile(filePath, 'utf8') };
    } catch (errorArg) {
      if (errorArg && typeof errorArg === 'object' && (errorArg as NodeJS.ErrnoException).code === 'ENOENT') {
        return { filePath, content: '' };
      }
      throw errorArg;
    }
  }

  private snapshotFromIcs(fileArg: { filePath: string; content: string }): ILocalTodoSnapshot {
    const parsed = parseIcsTodoList(fileArg.content);
    const needsActionCount = parsed.items.filter((itemArg) => itemArg.status === 'needs_action').length;
    const completedCount = parsed.items.filter((itemArg) => itemArg.status === 'completed').length;
    const entities: ISimpleLocalEntitySnapshot[] = [
      {
        id: 'todo_items',
        name: 'To-do Items',
        platform: 'sensor',
        state: parsed.items.length,
        available: true,
        attributes: {
          filePath: fileArg.filePath,
          prodid: parsed.prodid,
          items: parsed.items,
        },
      },
      {
        id: 'needs_action',
        name: 'Needs Action',
        platform: 'sensor',
        state: needsActionCount,
        available: true,
        attributes: { filePath: fileArg.filePath },
      },
      {
        id: 'completed',
        name: 'Completed',
        platform: 'sensor',
        state: completedCount,
        available: true,
        attributes: { filePath: fileArg.filePath },
      },
    ];

    return LocalTodoMapper.toSnapshot({
      config: this.configArg,
      source: 'manual',
      online: true,
      rawData: {
        device: {
          id: this.configArg.uniqueId || fileArg.filePath,
          name: this.configArg.name || this.configArg.todoListName || localTodoProfile.defaultName,
          manufacturer: localTodoProfile.manufacturer,
          model: localTodoProfile.model,
          protocol: 'local',
          attributes: {
            filePath: fileArg.filePath,
            prodid: parsed.prodid,
          },
        },
        entities,
        online: true,
        updatedAt: new Date().toISOString(),
        source: 'manual',
        rawData: {
          filePath: fileArg.filePath,
          prodid: parsed.prodid,
          todoItems: parsed.items,
          todoCount: parsed.items.length,
          needsActionCount,
          completedCount,
        },
      },
    });
  }

  private todoFilePath(): string | undefined {
    const metadata = this.configArg.metadata;
    return stringValue(this.configArg.filePath) || stringValue(this.configArg.icsPath) || stringValue(this.configArg.storagePath) || stringValue(this.configArg.todoFilePath) || stringValue(metadata?.filePath) || stringValue(metadata?.icsPath) || stringValue(metadata?.storagePath) || stringValue(metadata?.todoFilePath) || stringValue(this.configArg.path);
  }
}

interface ILocalTodoItem {
  uid?: string;
  summary: string;
  status: 'needs_action' | 'completed';
  due?: string;
  description?: string;
  completed?: string;
  rawStatus?: string;
}

interface IIcsProperty {
  key: string;
  params: Record<string, string>;
  value: string;
}

const localTodoProdidRequiresMigration = '-//homeassistant.io//local_todo 1.0//EN';

const parseIcsTodoList = (contentArg: string): { prodid?: string; items: ILocalTodoItem[] } => {
  const lines = unfoldIcsLines(contentArg);
  const items: ILocalTodoItem[] = [];
  let prodid: string | undefined;
  let currentTodo: IIcsProperty[] | undefined;

  for (const line of lines) {
    const property = parseIcsProperty(line);
    if (!property) {
      continue;
    }
    if (property.key === 'PRODID' && !currentTodo) {
      prodid = property.value;
      continue;
    }
    if (property.key === 'BEGIN' && property.value.toUpperCase() === 'VTODO') {
      currentTodo = [];
      continue;
    }
    if (property.key === 'END' && property.value.toUpperCase() === 'VTODO') {
      if (currentTodo) {
        items.push(todoFromProperties(currentTodo, prodid));
      }
      currentTodo = undefined;
      continue;
    }
    currentTodo?.push(property);
  }

  return { prodid, items };
};

const todoFromProperties = (propertiesArg: IIcsProperty[], prodidArg: string | undefined): ILocalTodoItem => {
  const property = (keyArg: string): IIcsProperty | undefined => propertiesArg.find((propertyArg) => propertyArg.key === keyArg);
  const status = property('STATUS')?.value.toUpperCase();
  const due = property('DUE');
  const completed = property('COMPLETED');
  const dateOnlyDue = due ? due.params.VALUE?.toUpperCase() === 'DATE' || /^\d{8}$/.test(due.value) : false;
  return {
    uid: property('UID')?.value,
    summary: unescapeIcsText(property('SUMMARY')?.value || 'Untitled'),
    status: status === 'COMPLETED' || status === 'CANCELLED' ? 'completed' : 'needs_action',
    due: due ? formatIcsDate(due.value, dateOnlyDue, dateOnlyDue && prodidArg !== localTodoProdidRequiresMigration) : undefined,
    description: property('DESCRIPTION') ? unescapeIcsText(property('DESCRIPTION')!.value) : undefined,
    completed: completed ? formatIcsDate(completed.value, false, false) : undefined,
    rawStatus: status,
  };
};

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

const parseIcsProperty = (lineArg: string): IIcsProperty | undefined => {
  const separatorIndex = lineArg.indexOf(':');
  if (separatorIndex < 0) {
    return undefined;
  }
  const nameAndParams = lineArg.slice(0, separatorIndex);
  const [name, ...params] = nameAndParams.split(';');
  const parsedParams: Record<string, string> = {};
  for (const param of params) {
    const [key, value] = param.split('=');
    if (key && value) {
      parsedParams[key.toUpperCase()] = value;
    }
  }
  return { key: name.toUpperCase(), params: parsedParams, value: lineArg.slice(separatorIndex + 1) };
};

const formatIcsDate = (valueArg: string, dateOnlyArg: boolean, subtractOneDayArg: boolean): string => {
  const dateOnlyMatch = /^(\d{4})(\d{2})(\d{2})$/.exec(valueArg);
  if (dateOnlyArg && dateOnlyMatch) {
    const date = new Date(Date.UTC(Number(dateOnlyMatch[1]), Number(dateOnlyMatch[2]) - 1, Number(dateOnlyMatch[3])));
    if (subtractOneDayArg) {
      date.setUTCDate(date.getUTCDate() - 1);
    }
    return date.toISOString().slice(0, 10);
  }

  const dateTimeMatch = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z?$/.exec(valueArg);
  if (dateTimeMatch) {
    return new Date(Date.UTC(Number(dateTimeMatch[1]), Number(dateTimeMatch[2]) - 1, Number(dateTimeMatch[3]), Number(dateTimeMatch[4]), Number(dateTimeMatch[5]), Number(dateTimeMatch[6]))).toISOString();
  }

  return valueArg;
};

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

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);
