import * as plugins from '../../plugins.js';
import { SimpleLocalClient, type IServiceCallRequest, type IServiceCallResult, type ISimpleLocalSnapshot } from '../../core/index.js';
import { KiraMapper } from './kira.mapper.js';
import type { IKiraCode, IKiraConfig } from './kira.types.js';
import { kiraProfile } from './kira.types.js';

export class KiraClientError extends Error {
  constructor(messageArg: string) {
    super(messageArg);
    this.name = 'KiraClientError';
  }
}

export class KiraClient extends SimpleLocalClient<IKiraConfig> {
  private fileSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly configArg: IKiraConfig) {
    super(kiraProfile, configArg);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<ISimpleLocalSnapshot> {
    if (!forceRefreshArg && this.fileSnapshot) {
      return clone(this.fileSnapshot);
    }

    if (this.configArg.client || this.configArg.snapshotProvider || (!forceRefreshArg && this.configArg.snapshot)) {
      return super.getSnapshot(forceRefreshArg);
    }

    const codesPath = this.codesPath();
    if (codesPath) {
      try {
        this.fileSnapshot = await this.readCodesSnapshot(codesPath);
        return clone(this.fileSnapshot);
      } catch (errorArg) {
        this.fileSnapshot = KiraMapper.toSnapshot({
          config: this.configArg,
          online: false,
          source: 'runtime',
          error: errorMessage(errorArg),
        });
        return clone(this.fileSnapshot);
      }
    }

    return super.getSnapshot(forceRefreshArg);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.fileSnapshot = undefined;
    const snapshot = await this.getSnapshot(true);
    const success = snapshot.online && !snapshot.error && snapshot.source !== 'runtime';
    return { success, error: success ? undefined : snapshot.error, data: snapshot };
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.client?.execute || this.configArg.commandExecutor) {
      return super.execute(requestArg);
    }
    return {
      success: false,
      error: `Kira ${requestArg.service} requires an injected client.execute() or commandExecutor; Home Assistant uses pykira over a UDP IR-IP bridge protocol, and this TypeScript integration only implements safe local code-file snapshots.`,
    };
  }

  private async readCodesSnapshot(codesPathArg: string): Promise<ISimpleLocalSnapshot> {
    const text = await plugins.fs.readFile(codesPathArg, 'utf8');
    const codes = parseKiraCodes(text);
    const rawData = {
      device: {
        id: this.configArg.uniqueId || this.configArg.deviceId || `kira_codes_${KiraMapper.slug(codesPathArg)}`,
        name: this.configArg.name || kiraProfile.defaultName,
        manufacturer: kiraProfile.manufacturer,
        model: 'Kira IR code file',
        attributes: {
          codesPath: codesPathArg,
          codeCount: codes.length,
        },
      },
      codes,
      entities: [
        {
          id: 'code_count',
          name: 'Code Count',
          platform: 'sensor' as const,
          state: codes.length,
          available: true,
          writable: false,
          attributes: { codesPath: codesPathArg },
        },
        ...codes.map((codeArg) => ({
          id: KiraMapper.slug(codeArg.name),
          uniqueId: `${this.configArg.uniqueId || 'kira'}_${KiraMapper.slug(codeArg.name)}`,
          name: codeArg.name,
          platform: 'button' as const,
          state: false,
          available: true,
          writable: false,
          attributes: cleanAttributes({
            device: codeArg.device,
            type: codeArg.type,
            repeat: codeArg.repeat,
            code: codeArg.code,
          }),
        })),
      ],
      fetchedAt: new Date().toISOString(),
    };

    return KiraMapper.toSnapshot({ config: this.configArg, rawData, online: true, source: 'client' });
  }

  private codesPath(): string | undefined {
    return stringValue(this.configArg.codesPath) || stringValue(this.configArg.path);
  }
}

const parseKiraCodes = (textArg: string): IKiraCode[] => {
  const trimmed = textArg.trim();
  if (!trimmed) {
    return [];
  }
  if (trimmed.startsWith('[')) {
    const parsed = JSON.parse(trimmed) as unknown;
    if (!Array.isArray(parsed)) {
      throw new KiraClientError('Kira code JSON must be an array.');
    }
    return parsed.map(normalizeCode).filter((codeArg): codeArg is IKiraCode => Boolean(codeArg));
  }

  const records: Record<string, unknown>[] = [];
  let current: Record<string, unknown> | undefined;
  for (const rawLine of textArg.split(/\r?\n/)) {
    const line = rawLine.replace(/\s+#.*$/, '').trimEnd();
    if (!line.trim()) {
      continue;
    }
    if (line.trimStart().startsWith('-')) {
      current = {};
      records.push(current);
      const rest = line.trimStart().slice(1).trim();
      if (rest) {
        assignYamlPair(current, rest);
      }
      continue;
    }
    if (!current) {
      continue;
    }
    assignYamlPair(current, line.trim());
  }

  return records.map(normalizeCode).filter((codeArg): codeArg is IKiraCode => Boolean(codeArg));
};

const assignYamlPair = (recordArg: Record<string, unknown>, lineArg: string): void => {
  const separator = lineArg.indexOf(':');
  if (separator < 0) {
    return;
  }
  const key = lineArg.slice(0, separator).trim();
  const value = lineArg.slice(separator + 1).trim();
  recordArg[key] = parseYamlScalar(value);
};

const parseYamlScalar = (valueArg: string): unknown => {
  const value = valueArg.trim();
  if (!value) {
    return '';
  }
  if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
    return value.slice(1, -1);
  }
  if (/^-?\d+$/.test(value)) {
    return Number(value);
  }
  return value;
};

const normalizeCode = (valueArg: unknown): IKiraCode | undefined => {
  if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
    return undefined;
  }
  const record = valueArg as Record<string, unknown>;
  const name = stringValue(record.name);
  const code = stringValue(record.code);
  if (!name || !code) {
    return undefined;
  }
  const repeat = numberValue(record.repeat);
  return {
    name,
    code,
    type: stringValue(record.type),
    device: stringValue(record.device),
    repeat,
  };
};

const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
const numberValue = (valueArg: unknown): number | undefined => typeof valueArg === 'number' && Number.isFinite(valueArg) ? Math.round(valueArg) : stringValue(valueArg) && Number.isFinite(Number(valueArg)) ? Math.round(Number(valueArg)) : undefined;
const cleanAttributes = (attributesArg: Record<string, unknown>): Record<string, unknown> => Object.fromEntries(Object.entries(attributesArg).filter(([, valueArg]) => valueArg !== undefined));
const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;
const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
