import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type {
  IDiscoveryCandidate,
  IDiscoveryContext,
  IDiscoveryMatch,
  IDiscoveryMatcher,
  IDiscoveryProbe,
  IDiscoveryProbeResult,
  IDiscoveryValidator,
} from '../../core/types.js';
import { DaikinMapper } from './daikin.mapper.js';
import type { IDaikinHttpCandidateRecord, IDaikinManualEntry, IDaikinMdnsRecord, IDaikinRawData, IDaikinSnapshot, IDaikinUdpDiscoveryRecord, IDaikinValueMap } from './daikin.types.js';
import { daikinDefaultHttpPort, daikinDisplayName, daikinDomain, daikinUdpDiscoveryMessage, daikinUdpDiscoveryPort, daikinUdpSourcePort, daikinZeroconfType } from './daikin.types.js';

export class DaikinUdpDiscoveryProbe implements IDiscoveryProbe {
  public id = 'daikin-udp-discovery-probe';
  public source = 'custom' as const;
  public description = 'Discover Daikin units using the local DAIKIN_UDP/common/basic_info broadcast used by pydaikin.';

  public async probe(contextArg: IDiscoveryContext): Promise<IDiscoveryProbeResult> {
    if (contextArg.abortSignal?.aborted) {
      return { candidates: [] };
    }
    return { candidates: await this.discover(1200) };
  }

  private async discover(timeoutMsArg: number): Promise<IDiscoveryCandidate[]> {
    const { createSocket } = await import('node:dgram');
    const matcher = new DaikinUdpMatcher();
    const candidates: IDiscoveryCandidate[] = [];
    const message = Buffer.from(daikinUdpDiscoveryMessage, 'utf8');

    return new Promise((resolve) => {
      const socket = createSocket({ type: 'udp4', reuseAddr: true });
      const timer = setTimeout(() => {
        closeSocket();
        resolve(candidates);
      }, timeoutMsArg);

      const closeSocket = () => {
        clearTimeout(timer);
        try {
          socket.close();
        } catch {
          // Discovery sockets may already be closed after timeout or OS errors.
        }
      };

      socket.on('message', async (dataArg, remoteArg) => {
        const match = await matcher.matches({ host: remoteArg.address, port: remoteArg.port, data: dataArg.toString('utf8') });
        if (match.matched && match.candidate && !candidates.some((candidateArg) => candidateArg.id === match.candidate?.id || candidateArg.host === match.candidate?.host)) {
          candidates.push(match.candidate);
        }
      });
      socket.on('error', () => {
        closeSocket();
        resolve(candidates);
      });
      socket.bind(daikinUdpSourcePort, () => {
        socket.setBroadcast(true);
        socket.send(message, daikinUdpDiscoveryPort, '255.255.255.255');
      });
    });
  }
}

export class DaikinZeroconfMatcher implements IDiscoveryMatcher<IDaikinMdnsRecord> {
  public id = 'daikin-zeroconf-match';
  public source = 'mdns' as const;
  public description = 'Recognize Daikin _dkapi._tcp.local zeroconf advertisements from the Home Assistant manifest.';

  public async matches(recordArg: IDaikinMdnsRecord): Promise<IDiscoveryMatch> {
    const txt = normalizeKeys({ ...recordArg.txt, ...recordArg.properties });
    const type = normalizeMdnsType(recordArg.type || recordArg.serviceType || '');
    const host = recordArg.host || recordArg.hostname || recordArg.addresses?.[0];
    const name = stringValue(txt.name) || cleanServiceName(recordArg.name) || daikinDisplayName;
    const mac = DaikinMapper.normalizeMac(txt.mac || txt.macaddress || txt.id);
    const matched = type === normalizeMdnsType(daikinZeroconfType) || type.includes('_dkapi') || recordArg.metadata?.daikin === true;
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'mDNS record is not a Daikin _dkapi advertisement.' };
    }
    return {
      matched: true,
      confidence: host && mac ? 'certain' : host ? 'high' : 'medium',
      reason: 'mDNS service matches _dkapi._tcp.local.',
      normalizedDeviceId: mac || recordArg.name || host,
      candidate: {
        source: 'mdns',
        integrationDomain: daikinDomain,
        id: mac || recordArg.name || host,
        host,
        port: recordArg.port || daikinDefaultHttpPort,
        name,
        manufacturer: 'Daikin',
        model: txt.model || txt.type || 'Daikin AC',
        macAddress: mac,
        metadata: {
          ...recordArg.metadata,
          daikin: true,
          discoveryProtocol: 'zeroconf',
          mdnsType: type,
          mdnsName: recordArg.name,
          txt,
        },
      },
    };
  }
}

export class DaikinUdpMatcher implements IDiscoveryMatcher<IDaikinUdpDiscoveryRecord> {
  public id = 'daikin-udp-match';
  public source = 'custom' as const;
  public description = 'Recognize pydaikin DAIKIN_UDP/basic_info discovery responses.';

  public async matches(recordArg: IDaikinUdpDiscoveryRecord): Promise<IDiscoveryMatch> {
    const values = this.values(recordArg);
    const host = recordArg.host || recordArg.address || recordArg.ip;
    const mac = DaikinMapper.normalizeMac(values.mac);
    const name = safeDecode(values.name) || (mac ? `Daikin ${mac.slice(-5).replace(':', '')}` : daikinDisplayName);
    const matched = Boolean(mac || values.type === 'aircon' || values.adp_kind || recordArg.metadata?.daikin === true);
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'UDP payload is not a Daikin discovery response.' };
    }
    return {
      matched: true,
      confidence: host && mac ? 'certain' : host ? 'high' : 'medium',
      reason: mac ? 'UDP response contains a Daikin MAC address.' : 'UDP response contains Daikin adapter metadata.',
      normalizedDeviceId: mac || host,
      candidate: {
        source: 'custom',
        integrationDomain: daikinDomain,
        id: mac || host,
        host,
        port: daikinDefaultHttpPort,
        name,
        manufacturer: 'Daikin',
        model: values.model || values.type || 'Daikin AC',
        macAddress: mac,
        metadata: {
          ...recordArg.metadata,
          daikin: true,
          discoveryProtocol: 'udp',
          discoveryPort: recordArg.port,
          values,
        },
      },
    };
  }

  private values(recordArg: IDaikinUdpDiscoveryRecord): IDaikinValueMap {
    const raw = recordArg.values || recordArg.response || recordArg.data || {};
    if (typeof raw === 'string') {
      try {
        return DaikinMapper.parseKeyValueResponse(raw);
      } catch {
        return {};
      }
    }
    return raw;
  }
}

export class DaikinHttpMatcher implements IDiscoveryMatcher<IDaikinHttpCandidateRecord> {
  public id = 'daikin-http-match';
  public source = 'http' as const;
  public description = 'Recognize local HTTP candidates that look like Daikin common/aircon, SkyFi, or dsiot endpoints.';

  public async matches(recordArg: IDaikinHttpCandidateRecord): Promise<IDiscoveryMatch> {
    const parsed = parseEndpoint(recordArg.url || recordArg.location);
    const headers = normalizeKeys(recordArg.headers || {});
    const path = recordArg.path || parsed?.path || '';
    const body = recordArg.body || recordArg.responseText || '';
    const text = [recordArg.url, recordArg.location, path, body, headers.server, recordArg.name, recordArg.manufacturer, recordArg.model].filter(Boolean).join(' ').toLowerCase();
    const values = this.parseBody(body);
    const mac = DaikinMapper.normalizeMac(values.mac);
    const matched = Boolean(recordArg.metadata?.daikin)
      || path.includes('/common/basic_info')
      || path.includes('/aircon/')
      || path.includes('/skyfi/')
      || path.includes('/dsiot/')
      || text.includes('daikin')
      || Boolean(mac && values.type === 'aircon');
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'HTTP candidate does not look like a Daikin local API.' };
    }
    const host = recordArg.host || parsed?.host;
    const port = recordArg.port || parsed?.port || daikinDefaultHttpPort;
    return {
      matched: true,
      confidence: host && (mac || path.includes('/common/basic_info')) ? 'high' : host ? 'medium' : 'low',
      reason: 'HTTP candidate has Daikin API path, body, or metadata hints.',
      normalizedDeviceId: mac || (host ? `${host}:${port}` : undefined),
      candidate: {
        source: 'http',
        integrationDomain: daikinDomain,
        id: mac || (host ? `${host}:${port}` : undefined),
        host,
        port,
        name: safeDecode(values.name) || recordArg.name || daikinDisplayName,
        manufacturer: recordArg.manufacturer || 'Daikin',
        model: recordArg.model || values.model || 'Daikin AC',
        macAddress: mac,
        metadata: {
          ...recordArg.metadata,
          daikin: true,
          url: recordArg.url || recordArg.location,
          ssl: recordArg.ssl ?? parsed?.ssl ?? false,
          values: Object.keys(values).length ? values : undefined,
          headers,
        },
      },
    };
  }

  private parseBody(valueArg: string): IDaikinValueMap {
    if (!valueArg.trim()) {
      return {};
    }
    try {
      return DaikinMapper.parseKeyValueResponse(valueArg);
    } catch {
      return {};
    }
  }
}

export class DaikinManualMatcher implements IDiscoveryMatcher<IDaikinManualEntry> {
  public id = 'daikin-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Daikin host, API key/password, snapshot, client, and raw-data setup entries.';

  public async matches(inputArg: IDaikinManualEntry): Promise<IDiscoveryMatch> {
    const metadata = inputArg.metadata || {};
    const parsed = parseEndpoint(inputArg.url || inputArg.host);
    const snapshot = snapshotValue(inputArg.snapshot || metadata.snapshot);
    const rawData = rawDataValue(inputArg.rawData || metadata.rawData);
    const hasManualData = Boolean(snapshot || rawData || inputArg.values || metadata.client);
    const text = [inputArg.name, inputArg.manufacturer, inputArg.model, metadata.name, metadata.manufacturer, metadata.model].filter(Boolean).join(' ').toLowerCase();
    const matched = Boolean(inputArg.host || inputArg.url || inputArg.apiKey || inputArg.password || metadata.daikin || hasManualData || text.includes('daikin'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain Daikin setup hints.' };
    }
    const host = parsed?.host || (inputArg.url ? undefined : inputArg.host) || snapshot?.device.host;
    const port = inputArg.port || parsed?.port || snapshot?.device.port || (inputArg.apiVariant === 'skyfi' || inputArg.password ? 2000 : daikinDefaultHttpPort);
    const id = inputArg.id || inputArg.uniqueId || snapshot?.device.id || snapshot?.device.macAddress || (host ? `${host}:${port}` : undefined);
    return {
      matched: true,
      confidence: host || hasManualData ? 'high' : 'medium',
      reason: 'Manual entry can start Daikin setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: daikinDomain,
        id,
        host,
        port,
        name: inputArg.name || snapshot?.device.name || daikinDisplayName,
        manufacturer: inputArg.manufacturer || snapshot?.device.manufacturer || 'Daikin',
        model: inputArg.model || snapshot?.device.model,
        macAddress: inputArg.macAddress || snapshot?.device.macAddress,
        metadata: {
          ...metadata,
          daikin: true,
          discoveryProtocol: 'manual',
          ssl: inputArg.ssl ?? inputArg.tls ?? parsed?.ssl ?? metadata.ssl,
          apiVariant: inputArg.apiVariant || metadata.apiVariant,
          apiKey: inputArg.apiKey || metadata.apiKey,
          uuid: inputArg.uuid || metadata.uuid,
          password: inputArg.password || metadata.password,
          snapshot,
          rawData,
          values: inputArg.values || metadata.values,
          client: inputArg.client || metadata.client,
          commandExecutor: inputArg.commandExecutor || metadata.commandExecutor,
        },
      },
    };
  }
}

export class DaikinCandidateValidator implements IDiscoveryValidator {
  public id = 'daikin-candidate-validator';
  public description = 'Validate Daikin candidates from zeroconf, UDP, HTTP, and manual setup.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const metadata = candidateArg.metadata || {};
    const snapshot = snapshotValue(metadata.snapshot);
    const rawData = rawDataValue(metadata.rawData);
    const mdnsType = typeof metadata.mdnsType === 'string' ? metadata.mdnsType.toLowerCase() : '';
    const discoveryProtocol = typeof metadata.discoveryProtocol === 'string' ? metadata.discoveryProtocol : undefined;
    const text = [candidateArg.integrationDomain, candidateArg.name, candidateArg.manufacturer, candidateArg.model, mdnsType].filter(Boolean).join(' ').toLowerCase();
    const matched = candidateArg.integrationDomain === daikinDomain
      || metadata.daikin === true
      || text.includes('daikin')
      || mdnsType.includes('_dkapi')
      || discoveryProtocol === 'udp';
    const hasUsableSource = Boolean(candidateArg.host || snapshot || rawData || metadata.values || metadata.client);
    const normalizedDeviceId = DaikinMapper.normalizeMac(candidateArg.macAddress) || candidateArg.id || snapshot?.device.macAddress || (candidateArg.host ? `${candidateArg.host}:${candidateArg.port || daikinDefaultHttpPort}` : undefined);
    if (!matched || !hasUsableSource) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'Daikin candidate lacks a host, injected client, snapshot, or manual data.' : 'Candidate is not Daikin.',
        normalizedDeviceId,
      };
    }
    return {
      matched: true,
      confidence: candidateArg.host && normalizedDeviceId ? 'certain' : candidateArg.host ? 'high' : 'medium',
      reason: 'Candidate has Daikin metadata and a usable local endpoint, client, snapshot, or manual data.',
      normalizedDeviceId,
      candidate: {
        ...candidateArg,
        integrationDomain: daikinDomain,
        port: candidateArg.port || daikinDefaultHttpPort,
        manufacturer: candidateArg.manufacturer || 'Daikin',
      },
    };
  }
}

export const createDaikinDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: daikinDomain, displayName: daikinDisplayName })
    .addProbe(new DaikinUdpDiscoveryProbe())
    .addMatcher(new DaikinZeroconfMatcher())
    .addMatcher(new DaikinUdpMatcher())
    .addMatcher(new DaikinHttpMatcher())
    .addMatcher(new DaikinManualMatcher())
    .addValidator(new DaikinCandidateValidator());
};

const normalizeMdnsType = (valueArg: string): string => valueArg.toLowerCase().replace(/\.$/, '');

const cleanServiceName = (valueArg: string | undefined): string | undefined => {
  if (!valueArg) {
    return undefined;
  }
  return valueArg.replace(/\._.*$/u, '').replace(/\.local\.?$/i, '').trim() || undefined;
};

const parseEndpoint = (valueArg: string | undefined): { host: string; port?: number; ssl: boolean; path: string } | undefined => {
  if (!valueArg || !/^[a-z][a-z0-9+.-]*:\/\//i.test(valueArg)) {
    return undefined;
  }
  try {
    const url = new URL(valueArg);
    return { host: url.hostname, port: url.port ? Number(url.port) : undefined, ssl: url.protocol === 'https:', path: url.pathname };
  } catch {
    return undefined;
  }
};

const normalizeKeys = (recordArg: Record<string, string | undefined>): Record<string, string | undefined> => {
  const normalized: Record<string, string | undefined> = {};
  for (const [key, value] of Object.entries(recordArg)) {
    normalized[key.toLowerCase()] = value;
  }
  return normalized;
};

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

const safeDecode = (valueArg: string | undefined): string | undefined => {
  if (valueArg === undefined) {
    return undefined;
  }
  try {
    return decodeURIComponent(valueArg.replace(/\+/g, '%20'));
  } catch {
    return valueArg;
  }
};

const snapshotValue = (valueArg: unknown): IDaikinSnapshot | undefined => {
  return valueArg && typeof valueArg === 'object' && 'device' in valueArg && 'climate' in valueArg ? valueArg as IDaikinSnapshot : undefined;
};

const rawDataValue = (valueArg: unknown): Partial<IDaikinRawData> | undefined => {
  return valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Partial<IDaikinRawData> : undefined;
};
