import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryContext, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IFreeboxManualEntry, IFreeboxRawData, IFreeboxSnapshot, IFreeboxZeroconfRecord } from './freebox.types.js';
import { freeboxDefaultApiBaseUrl, freeboxDefaultApiVersion, freeboxDefaultPort, freeboxDisplayName, freeboxDomain, freeboxManufacturer, freeboxZeroconfType } from './freebox.types.js';

export class FreeboxZeroconfMatcher implements IDiscoveryMatcher<IFreeboxZeroconfRecord> {
  public id = 'freebox-zeroconf-match';
  public source = 'mdns' as const;
  public description = 'Recognize Freebox API mDNS service records using _fbx-api._tcp.local. and API TXT metadata.';

  public async matches(recordArg: IFreeboxZeroconfRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = recordArg.metadata || {};
    const properties = { ...(recordArg.txt || {}), ...(recordArg.properties || {}) };
    const serviceType = stringValue(recordArg.type) || stringValue(recordArg.serviceType) || stringValue(metadata.type) || stringValue(metadata.serviceType);
    const serviceName = stringValue(recordArg.name) || stringValue(metadata.name) || '';
    const apiDomain = stringValue(properties.api_domain) || stringValue(metadata.api_domain);
    const uid = stringValue(properties.uid) || stringValue(metadata.uid);
    const apiBaseUrl = stringValue(properties.api_base_url) || stringValue(metadata.api_base_url) || freeboxDefaultApiBaseUrl;
    const apiVersion = stringValue(properties.api_version) || stringValue(metadata.api_version) || freeboxDefaultApiVersion;
    const deviceType = stringValue(properties.device_type) || stringValue(metadata.device_type);
    const host = apiDomain || stringValue(recordArg.host) || stringValue(recordArg.hostname) || firstString(recordArg.addresses) || stringValue(metadata.host);
    const httpsPort = numberValue(properties.https_port) || numberValue(metadata.https_port);
    const port = httpsPort || numberValue(recordArg.port) || freeboxDefaultPort;
    const matched = serviceType === freeboxZeroconfType || serviceType === '_fbx-api._tcp' || Boolean(apiDomain || uid || serviceName.toLowerCase().includes('freebox'));

    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'mDNS record is not a Freebox API service.' };
    }

    return {
      matched: true,
      confidence: host && uid ? 'certain' : host ? 'high' : 'medium',
      reason: serviceType?.includes('_fbx-api') ? 'mDNS service type matches the Home Assistant Freebox zeroconf matcher _fbx-api._tcp.local.' : 'mDNS TXT metadata contains Freebox API fields.',
      normalizedDeviceId: uid || host,
      candidate: {
        source: 'mdns',
        integrationDomain: freeboxDomain,
        id: uid || host,
        host,
        port,
        name: stringValue(properties.device_name) || serviceName || freeboxDisplayName,
        manufacturer: freeboxManufacturer,
        model: deviceType,
        metadata: {
          ...metadata,
          ...properties,
          freebox: true,
          discoveryProtocol: 'zeroconf',
          serviceType: serviceType || freeboxZeroconfType,
          apiDomain,
          apiBaseUrl,
          apiVersion: normalizeApiVersion(apiVersion),
          uid,
          httpsAvailable: booleanValue(properties.https_available ?? metadata.https_available),
        },
      },
    };
  }
}

export class FreeboxManualMatcher implements IDiscoveryMatcher<IFreeboxManualEntry> {
  public id = 'freebox-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Freebox host, snapshot, raw-data, injected-client, and command-executor entries.';

  public async matches(inputArg: IFreeboxManualEntry, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = inputArg.metadata || {};
    const parsed = parseEndpoint(inputArg.url || inputArg.host || stringValue(metadata.url) || stringValue(metadata.host));
    const snapshot = snapshotValue(inputArg.snapshot || metadata.snapshot);
    const rawData = rawDataValue(inputArg.rawData || metadata.rawData);
    const hasManualData = Boolean(snapshot || rawData || inputArg.client || metadata.client || inputArg.commandExecutor || metadata.commandExecutor);
    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 || metadata.freebox === true || hasManualData || text.includes('freebox') || text.includes('fbx'));

    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain Freebox setup hints.' };
    }

    const host = parsed?.host || inputArg.host || snapshot?.router.host || stringValue(metadata.host);
    const port = inputArg.port || parsed?.port || snapshot?.router.port || numberValue(metadata.port) || freeboxDefaultPort;
    const id = inputArg.id || inputArg.uniqueId || inputArg.macAddress || snapshot?.router.macAddress || snapshot?.router.id || (host ? `${host}:${port}` : undefined);
    const apiVersion = inputArg.apiVersion || stringValue(metadata.apiVersion) || snapshot?.router.apiVersion || freeboxDefaultApiVersion;

    return {
      matched: true,
      confidence: host || hasManualData ? 'high' : 'medium',
      reason: 'Manual entry can start Freebox setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: freeboxDomain,
        id,
        host,
        port,
        name: inputArg.name || snapshot?.router.name || freeboxDisplayName,
        manufacturer: inputArg.manufacturer || snapshot?.router.manufacturer || freeboxManufacturer,
        model: inputArg.model || snapshot?.router.model,
        serialNumber: inputArg.serialNumber || snapshot?.router.serialNumber,
        macAddress: inputArg.macAddress || snapshot?.router.macAddress,
        metadata: {
          ...metadata,
          freebox: true,
          discoveryProtocol: 'manual',
          protocol: inputArg.protocol || parsed?.protocol || snapshot?.router.protocol || metadata.protocol,
          apiVersion: normalizeApiVersion(apiVersion),
          apiBaseUrl: inputArg.apiBaseUrl || snapshot?.router.apiBaseUrl || stringValue(metadata.apiBaseUrl) || freeboxDefaultApiBaseUrl,
          appToken: inputArg.appToken || inputArg.token || metadata.appToken || metadata.token,
          sessionToken: inputArg.sessionToken || metadata.sessionToken,
          snapshot,
          rawData,
          client: inputArg.client || metadata.client,
          commandExecutor: inputArg.commandExecutor || metadata.commandExecutor,
        },
      },
    };
  }
}

export class FreeboxCandidateValidator implements IDiscoveryValidator {
  public id = 'freebox-candidate-validator';
  public description = 'Validate Freebox candidates from zeroconf and manual setup.';

  public async validate(candidateArg: IDiscoveryCandidate, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = candidateArg.metadata || {};
    const snapshot = snapshotValue(metadata.snapshot);
    const rawData = rawDataValue(metadata.rawData);
    const serviceType = stringValue(metadata.serviceType) || stringValue(metadata.type);
    const text = [candidateArg.integrationDomain, candidateArg.name, candidateArg.manufacturer, candidateArg.model, metadata.name, metadata.manufacturer, metadata.model, metadata.device_type].filter(Boolean).join(' ').toLowerCase();
    const matched = candidateArg.integrationDomain === freeboxDomain
      || metadata.freebox === true
      || serviceType === freeboxZeroconfType
      || serviceType === '_fbx-api._tcp'
      || text.includes('freebox')
      || Boolean(metadata.api_domain || metadata.uid || metadata.api_base_url);
    const hasUsableSource = Boolean(candidateArg.host || snapshot || rawData || metadata.client);
    const normalizedDeviceId = candidateArg.id || stringValue(metadata.uid) || snapshot?.router.macAddress || snapshot?.router.id || (candidateArg.host ? `${candidateArg.host}:${candidateArg.port || freeboxDefaultPort}` : undefined);

    if (!matched || !hasUsableSource) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'Freebox candidate lacks a host, injected client, snapshot, or raw data.' : 'Candidate is not Freebox.',
        normalizedDeviceId,
      };
    }

    return {
      matched: true,
      confidence: candidateArg.host && normalizedDeviceId ? 'certain' : candidateArg.host ? 'high' : 'medium',
      reason: 'Candidate has Freebox metadata and a usable local endpoint, client, snapshot, or raw data.',
      normalizedDeviceId,
      candidate: {
        ...candidateArg,
        integrationDomain: freeboxDomain,
        port: candidateArg.port || freeboxDefaultPort,
        manufacturer: candidateArg.manufacturer || freeboxManufacturer,
        metadata: {
          ...metadata,
          freebox: true,
          apiVersion: normalizeApiVersion(stringValue(metadata.apiVersion) || stringValue(metadata.api_version) || snapshot?.router.apiVersion || freeboxDefaultApiVersion),
          apiBaseUrl: stringValue(metadata.apiBaseUrl) || stringValue(metadata.api_base_url) || snapshot?.router.apiBaseUrl || freeboxDefaultApiBaseUrl,
        },
      },
    };
  }
}

export const createFreeboxDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: freeboxDomain, displayName: freeboxDisplayName })
    .addMatcher(new FreeboxZeroconfMatcher())
    .addMatcher(new FreeboxManualMatcher())
    .addValidator(new FreeboxCandidateValidator());
};

const snapshotValue = (valueArg: unknown): IFreeboxSnapshot | undefined => valueArg && typeof valueArg === 'object' && 'router' in valueArg && 'sensors' in valueArg ? valueArg as IFreeboxSnapshot : undefined;

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

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

const firstString = (valueArg: unknown): string | undefined => Array.isArray(valueArg) ? valueArg.find((itemArg) => typeof itemArg === 'string' && itemArg.trim()) : undefined;

const numberValue = (valueArg: unknown): number | undefined => {
  if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
    return Math.round(valueArg);
  }
  if (typeof valueArg === 'string' && valueArg.trim()) {
    const parsed = Number(valueArg);
    return Number.isFinite(parsed) ? Math.round(parsed) : undefined;
  }
  return undefined;
};

const booleanValue = (valueArg: unknown): boolean | undefined => {
  if (typeof valueArg === 'boolean') {
    return valueArg;
  }
  if (typeof valueArg === 'string') {
    const normalized = valueArg.trim().toLowerCase();
    if (['1', 'true', 'yes', 'on'].includes(normalized)) {
      return true;
    }
    if (['0', 'false', 'no', 'off'].includes(normalized)) {
      return false;
    }
  }
  return undefined;
};

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

const normalizeApiVersion = (valueArg: string): string => valueArg.startsWith('v') ? valueArg : `v${valueArg.split('.')[0] || '6'}`;
