import * as plugins from '../../plugins.js';
import type { IIntegrationEntity, IIntegrationEvent, TEntityPlatform } from '../../core/types.js';
import type {
  IFritzboxCallmonitorCallAttributes,
  IFritzboxCallmonitorCallEvent,
  IFritzboxCallmonitorConfig,
  IFritzboxCallmonitorContact,
  IFritzboxCallmonitorContactInput,
  IFritzboxCallmonitorMonitorInfo,
  IFritzboxCallmonitorSnapshot,
  TFritzboxCallmonitorCallState,
  TFritzboxCallmonitorEventInput,
  TFritzboxCallmonitorFritzState,
  TFritzboxCallmonitorSnapshotSource,
  IFritzboxCallmonitorClientEvent,
} from './fritzbox_callmonitor.types.js';
import {
  fritzboxCallmonitorDefaultHost,
  fritzboxCallmonitorDefaultName,
  fritzboxCallmonitorDefaultPort,
  fritzboxCallmonitorDomain,
  fritzboxCallmonitorManufacturer,
} from './fritzbox_callmonitor.types.js';

const unknownContact: IFritzboxCallmonitorContact = { name: 'unknown', numbers: [], vip: false };
const callStateOptions: TFritzboxCallmonitorCallState[] = ['idle', 'ringing', 'dialing', 'talking'];

export class FritzboxCallmonitorMapper {
  public static toSnapshot(configArg: IFritzboxCallmonitorConfig, connectedArg?: boolean, extraEventsArg: TFritzboxCallmonitorEventInput[] = []): IFritzboxCallmonitorSnapshot {
    const source = configArg.snapshot;
    const contacts = this.uniqueContacts([
      ...(source?.contacts || []),
      ...this.normalizeContacts(configArg.contacts || []),
    ]);
    const prefixes = this.normalizePrefixes(configArg.prefixes || source?.prefixes || []);
    const monitor = this.monitorInfo(configArg, source?.monitor);
    const events = this.eventInputsToEvents([
      ...(source?.events || []),
      ...(configArg.events || []),
      ...(configArg.eventLines || []),
      ...extraEventsArg,
    ], contacts, prefixes);
    const lastEvent = events[events.length - 1] || source?.call.lastEvent;
    const call = {
      state: lastEvent?.callState || source?.call.state || 'idle' as TFritzboxCallmonitorCallState,
      attributes: this.cleanAttributes({
        ...(source?.call.attributes || {}),
        ...(lastEvent?.attributes || {}),
      }) as IFritzboxCallmonitorCallAttributes,
      lastEvent,
    };
    const hasManualState = Boolean(source || events.length || configArg.connected !== undefined);
    const hasManualData = Boolean(hasManualState || contacts.length);

    return {
      connected: connectedArg ?? configArg.connected ?? source?.connected ?? hasManualState,
      source: source?.source || (hasManualData ? 'manual' : 'runtime'),
      updatedAt: source?.updatedAt || new Date().toISOString(),
      monitor,
      call,
      contacts,
      prefixes,
      events,
      error: source?.error,
      metadata: this.cleanAttributes({
        ...source?.metadata,
        ...configArg.metadata,
        nativeTcpCallMonitor: true,
        phonebookApiImplemented: false,
        liveEventsConfigured: Boolean(configArg.host || configArg.nativeClient?.subscribe || configArg.eventSource || configArg.eventExecutor),
      }),
    };
  }

  public static normalizeSnapshot(snapshotArg: IFritzboxCallmonitorSnapshot, configArg: IFritzboxCallmonitorConfig = {}, sourceArg?: TFritzboxCallmonitorSnapshotSource): IFritzboxCallmonitorSnapshot {
    const normalized = this.toSnapshot({ ...configArg, snapshot: snapshotArg }, snapshotArg.connected);
    return {
      ...normalized,
      source: sourceArg || snapshotArg.source || normalized.source,
      updatedAt: snapshotArg.updatedAt || normalized.updatedAt,
      error: snapshotArg.error,
    };
  }

  public static toDevices(snapshotArg: IFritzboxCallmonitorSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
    return [{
      id: this.deviceId(snapshotArg),
      integrationDomain: fritzboxCallmonitorDomain,
      name: this.deviceName(snapshotArg),
      protocol: 'unknown',
      manufacturer: snapshotArg.monitor.manufacturer || fritzboxCallmonitorManufacturer,
      model: snapshotArg.monitor.model || 'FRITZ!Box Call Monitor',
      online: snapshotArg.connected,
      features: [
        { id: 'connectivity', capability: 'sensor', name: 'Connectivity', readable: true, writable: false },
        { id: 'call_state', capability: 'sensor', name: 'Call state', readable: true, writable: false },
        { id: 'last_event', capability: 'sensor', name: 'Last call event', readable: true, writable: false },
      ],
      state: [
        { featureId: 'connectivity', value: snapshotArg.connected ? 'online' : 'offline', updatedAt },
        { featureId: 'call_state', value: snapshotArg.call.state, updatedAt },
        { featureId: 'last_event', value: snapshotArg.call.lastEvent?.fritzState || null, updatedAt },
      ],
      metadata: this.cleanAttributes({
        host: snapshotArg.monitor.host,
        port: snapshotArg.monitor.port,
        phonebookName: snapshotArg.monitor.phonebookName,
        phonebookId: snapshotArg.monitor.phonebookId,
        serialNumber: snapshotArg.monitor.serialNumber,
        source: snapshotArg.source,
        prefixes: snapshotArg.prefixes,
        phonebookApiImplemented: false,
      }),
    }];
  }

  public static toEntities(snapshotArg: IFritzboxCallmonitorSnapshot): IIntegrationEntity[] {
    const deviceId = this.deviceId(snapshotArg);
    const base = this.slug(this.deviceName(snapshotArg));
    const uniqueBase = this.uniqueBase(snapshotArg);
    return [
      this.entity('binary_sensor', `${this.deviceName(snapshotArg)} Call monitor connection`, deviceId, `${uniqueBase}_connection`, snapshotArg.connected ? 'on' : 'off', snapshotArg.connected, {
        deviceClass: 'connectivity',
        host: snapshotArg.monitor.host,
        port: snapshotArg.monitor.port,
        source: snapshotArg.source,
      }, `binary_sensor.${base}_call_monitor_connection`),
      this.entity('sensor', `${this.deviceName(snapshotArg)} Call monitor`, deviceId, `${uniqueBase}_call_monitor`, snapshotArg.call.state, snapshotArg.connected || snapshotArg.source === 'manual' || snapshotArg.events.length > 0, {
        ...snapshotArg.call.attributes,
        options: callStateOptions,
        prefixes: snapshotArg.prefixes?.length ? snapshotArg.prefixes : undefined,
        nativeFritzState: snapshotArg.call.lastEvent?.fritzState,
        callId: snapshotArg.call.lastEvent?.callId,
        source: snapshotArg.source,
        eventCount: snapshotArg.events.length,
      }, `sensor.${base}_call_monitor`),
    ];
  }

  public static toIntegrationEvent(eventArg: IFritzboxCallmonitorClientEvent): IIntegrationEvent {
    const snapshot = eventArg.snapshot;
    return {
      type: eventArg.type === 'monitor_failed' || eventArg.type === 'event_parse_failed' ? 'error' : 'state_changed',
      integrationDomain: fritzboxCallmonitorDomain,
      deviceId: snapshot ? this.deviceId(snapshot) : undefined,
      entityId: snapshot ? `sensor.${this.slug(this.deviceName(snapshot))}_call_monitor` : undefined,
      data: this.cleanAttributes({
        type: eventArg.type,
        callEvent: eventArg.callEvent,
        raw: eventArg.raw,
        error: eventArg.error,
        snapshot,
        data: eventArg.data,
      }),
      timestamp: eventArg.timestamp,
    };
  }

  public static parseEvent(eventArg: TFritzboxCallmonitorEventInput, contactsArg: IFritzboxCallmonitorContact[] = [], prefixesArg: string[] = []): IFritzboxCallmonitorCallEvent {
    if (typeof eventArg !== 'string') {
      return this.normalizeCallEvent(eventArg);
    }

    const raw = eventArg.trim();
    const line = raw.split(';');
    const fritzState = line[1] as TFritzboxCallmonitorFritzState | undefined;
    const timestamp = this.isoTimestamp(line[0]);
    if (!timestamp || !this.isFritzState(fritzState)) {
      throw new Error(`Invalid FRITZ!Box call monitor event: ${eventArg}`);
    }

    if (fritzState === 'RING') {
      this.assertLength(line, 6, eventArg);
      const contact = this.contactForNumber(line[3], contactsArg, prefixesArg);
      return this.event(raw, line, fritzState, 'ringing', 'incoming', line[2], timestamp, {
        type: 'incoming',
        from: line[3],
        to: line[4],
        device: line[5],
        initiated: timestamp,
        from_name: contact.name,
        vip: contact.vip,
      });
    }

    if (fritzState === 'CALL') {
      this.assertLength(line, 7, eventArg);
      const contact = this.contactForNumber(line[5], contactsArg, prefixesArg);
      return this.event(raw, line, fritzState, 'dialing', 'outgoing', line[2], timestamp, {
        type: 'outgoing',
        from: line[4],
        to: line[5],
        device: line[6],
        initiated: timestamp,
        to_name: contact.name,
        vip: contact.vip,
      });
    }

    if (fritzState === 'CONNECT') {
      this.assertLength(line, 5, eventArg);
      const contact = this.contactForNumber(line[4], contactsArg, prefixesArg);
      return this.event(raw, line, fritzState, 'talking', 'connected', line[2], timestamp, {
        with: line[4],
        device: line[3],
        accepted: timestamp,
        with_name: contact.name,
        vip: contact.vip,
      });
    }

    this.assertLength(line, 4, eventArg);
    return this.event(raw, line, fritzState, 'idle', 'disconnected', line[2], timestamp, {
      duration: line[3],
      closed: timestamp,
    });
  }

  public static contactForNumber(numberArg: string | undefined, contactsArg: IFritzboxCallmonitorContact[], prefixesArg: string[] = []): IFritzboxCallmonitorContact {
    const number = this.normalizeNumber(numberArg || '');
    if (!number) {
      return unknownContact;
    }
    const byNumber = new Map<string, IFritzboxCallmonitorContact>();
    for (const contact of contactsArg) {
      for (const contactNumber of contact.numbers) {
        byNumber.set(this.normalizeNumber(contactNumber), contact);
      }
    }

    const direct = byNumber.get(number);
    if (direct) {
      return direct;
    }

    for (const prefix of prefixesArg.map((prefixArg) => this.normalizeNumber(prefixArg)).filter(Boolean)) {
      const prefixed = byNumber.get(`${prefix}${number}`) || byNumber.get(`${prefix}${number.replace(/^0+/, '')}`);
      if (prefixed) {
        return prefixed;
      }
    }

    return unknownContact;
  }

  public static normalizeContacts(contactsArg: IFritzboxCallmonitorContactInput[] | IFritzboxCallmonitorContact[]): IFritzboxCallmonitorContact[] {
    return contactsArg
      .map((contactArg) => this.normalizeContact(contactArg))
      .filter((contactArg): contactArg is IFritzboxCallmonitorContact => Boolean(contactArg));
  }

  public static normalizeNumber(valueArg: string): string {
    return String(valueArg || '').replace(/[^\d+]/g, '');
  }

  public static slug(valueArg: string | undefined): string {
    return String(valueArg || fritzboxCallmonitorDomain).toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || fritzboxCallmonitorDomain;
  }

  public static deviceId(snapshotArg: IFritzboxCallmonitorSnapshot): string {
    return `${fritzboxCallmonitorDomain}.monitor.${this.uniqueBase(snapshotArg)}`;
  }

  public static deviceName(snapshotArg: IFritzboxCallmonitorSnapshot): string {
    return snapshotArg.monitor.name || snapshotArg.monitor.phonebookName || fritzboxCallmonitorDefaultName;
  }

  public static uniqueBase(snapshotArg: IFritzboxCallmonitorSnapshot): string {
    return this.slug(snapshotArg.monitor.serialNumber || snapshotArg.monitor.id || (snapshotArg.monitor.host ? `${snapshotArg.monitor.host}_${snapshotArg.monitor.port || fritzboxCallmonitorDefaultPort}` : undefined) || this.deviceName(snapshotArg));
  }

  private static monitorInfo(configArg: IFritzboxCallmonitorConfig, sourceArg?: IFritzboxCallmonitorMonitorInfo): IFritzboxCallmonitorMonitorInfo {
    const host = configArg.host || sourceArg?.host;
    const port = configArg.port || sourceArg?.port || (host ? fritzboxCallmonitorDefaultPort : undefined);
    const id = configArg.uniqueId || sourceArg?.id || configArg.serialNumber || sourceArg?.serialNumber || (host ? `${host}:${port}` : undefined) || configArg.phonebookName || sourceArg?.phonebookName || fritzboxCallmonitorDefaultName;
    return this.cleanAttributes({
      ...sourceArg,
      id,
      host,
      port,
      name: configArg.name || sourceArg?.name || configArg.phonebookName || sourceArg?.phonebookName || fritzboxCallmonitorDefaultName,
      phonebookName: configArg.phonebookName || sourceArg?.phonebookName,
      phonebookId: configArg.phonebookId ?? sourceArg?.phonebookId,
      username: configArg.username || sourceArg?.username,
      serialNumber: configArg.serialNumber || sourceArg?.serialNumber,
      manufacturer: sourceArg?.manufacturer || fritzboxCallmonitorManufacturer,
      model: configArg.model || sourceArg?.model || 'FRITZ!Box Call Monitor',
      configurationUrl: sourceArg?.configurationUrl || (host ? `http://${host}` : undefined),
      metadata: this.cleanAttributes({
        ...sourceArg?.metadata,
        passwordConfigured: Boolean(configArg.password),
      }),
    }) as IFritzboxCallmonitorMonitorInfo;
  }

  private static normalizeContact(contactArg: IFritzboxCallmonitorContactInput | IFritzboxCallmonitorContact): IFritzboxCallmonitorContact | undefined {
    const name = this.stringValue(contactArg.name) || 'unknown';
    const rawNumbers = [
      ...(Array.isArray(contactArg.numbers) ? contactArg.numbers : []),
      ...('number' in contactArg && typeof contactArg.number === 'string' ? [contactArg.number] : []),
    ];
    const numbers = [...new Set(rawNumbers.map((numberArg) => this.normalizeNumber(numberArg)).filter(Boolean))];
    if (!numbers.length && name === 'unknown') {
      return undefined;
    }
    return {
      name,
      numbers,
      vip: contactArg.vip === true || contactArg.category === '1',
      category: contactArg.category,
    };
  }

  private static uniqueContacts(contactsArg: IFritzboxCallmonitorContact[]): IFritzboxCallmonitorContact[] {
    const byKey = new Map<string, IFritzboxCallmonitorContact>();
    for (const contact of contactsArg) {
      const key = `${contact.name}:${contact.numbers.join(',')}`;
      byKey.set(key, contact);
    }
    return [...byKey.values()];
  }

  private static normalizePrefixes(prefixesArg: string[]): string[] {
    return [...new Set(prefixesArg.map((prefixArg) => prefixArg.trim()).filter(Boolean))];
  }

  private static eventInputsToEvents(eventsArg: TFritzboxCallmonitorEventInput[], contactsArg: IFritzboxCallmonitorContact[], prefixesArg: string[]): IFritzboxCallmonitorCallEvent[] {
    const events: IFritzboxCallmonitorCallEvent[] = [];
    for (const eventInput of eventsArg) {
      try {
        events.push(this.parseEvent(eventInput, contactsArg, prefixesArg));
      } catch {
        // Snapshots are allowed to contain partial manual history; explicit parse calls still throw.
      }
    }
    return events;
  }

  private static normalizeCallEvent(eventArg: IFritzboxCallmonitorCallEvent): IFritzboxCallmonitorCallEvent {
    if (eventArg.type !== 'call_event' || !this.isFritzState(eventArg.fritzState)) {
      throw new Error('Invalid FRITZ!Box call monitor event object.');
    }
    return {
      ...eventArg,
      callState: eventArg.callState || this.callStateForFritzState(eventArg.fritzState),
      direction: eventArg.direction || 'unknown',
      attributes: this.cleanAttributes(eventArg.attributes || {}) as IFritzboxCallmonitorCallAttributes,
    };
  }

  private static event(
    rawArg: string,
    lineArg: string[],
    fritzStateArg: TFritzboxCallmonitorFritzState,
    callStateArg: TFritzboxCallmonitorCallState,
    directionArg: IFritzboxCallmonitorCallEvent['direction'],
    callIdArg: string | undefined,
    timestampArg: string,
    attributesArg: IFritzboxCallmonitorCallAttributes,
  ): IFritzboxCallmonitorCallEvent {
    return {
      type: 'call_event',
      raw: rawArg,
      line: lineArg,
      fritzState: fritzStateArg,
      callState: callStateArg,
      direction: directionArg,
      callId: callIdArg,
      timestamp: timestampArg,
      attributes: this.cleanAttributes(attributesArg) as IFritzboxCallmonitorCallAttributes,
    };
  }

  private static entity<TState>(platformArg: TEntityPlatform, nameArg: string, deviceIdArg: string, uniqueIdArg: string, stateArg: TState, availableArg: boolean, attributesArg: Record<string, unknown>, idArg: string): IIntegrationEntity<TState> {
    return {
      id: idArg,
      uniqueId: `${fritzboxCallmonitorDomain}_${uniqueIdArg}`,
      integrationDomain: fritzboxCallmonitorDomain,
      deviceId: deviceIdArg,
      platform: platformArg,
      name: nameArg,
      state: stateArg,
      attributes: this.cleanAttributes(attributesArg),
      available: availableArg,
    };
  }

  private static isFritzState(valueArg: unknown): valueArg is TFritzboxCallmonitorFritzState {
    return valueArg === 'RING' || valueArg === 'CALL' || valueArg === 'CONNECT' || valueArg === 'DISCONNECT';
  }

  private static callStateForFritzState(valueArg: TFritzboxCallmonitorFritzState): TFritzboxCallmonitorCallState {
    if (valueArg === 'RING') {
      return 'ringing';
    }
    if (valueArg === 'CALL') {
      return 'dialing';
    }
    if (valueArg === 'CONNECT') {
      return 'talking';
    }
    return 'idle';
  }

  private static assertLength(lineArg: string[], lengthArg: number, rawArg: string): void {
    if (lineArg.length < lengthArg) {
      throw new Error(`Incomplete FRITZ!Box call monitor event: ${rawArg}`);
    }
  }

  private static isoTimestamp(valueArg: string | undefined): string | undefined {
    const match = /^(\d{2})\.(\d{2})\.(\d{2}) (\d{2}):(\d{2}):(\d{2})$/.exec(valueArg || '');
    if (!match) {
      return undefined;
    }
    const year = Number(match[3]) >= 70 ? `19${match[3]}` : `20${match[3]}`;
    return `${year}-${match[2]}-${match[1]}T${match[4]}:${match[5]}:${match[6]}`;
  }

  private static stringValue(valueArg: unknown): string | undefined {
    return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
  }

  private static cleanAttributes<T extends Record<string, unknown>>(attributesArg: T): Record<string, unknown> {
    return Object.fromEntries(Object.entries(attributesArg).filter(([, valueArg]) => valueArg !== undefined));
  }
}
