import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryContext, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import { DoorbirdMapper } from './doorbird.mapper.js';
import type { IDoorbirdManualEntry, IDoorbirdMdnsRecord, TDoorbirdProtocol } from './doorbird.types.js';
import { doorbirdDefaultHttpsPort, doorbirdDefaultPort, doorbirdDomain, doorbirdManufacturer, doorbirdMdnsType, doorbirdOui } from './doorbird.types.js';

export class DoorbirdManualMatcher implements IDiscoveryMatcher<IDoorbirdManualEntry> {
  public id = 'doorbird-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual DoorBird local HTTP setup entries.';

  public async matches(inputArg: IDoorbirdManualEntry, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const endpoint = endpointFromManual(inputArg);
    const metadata = inputArg.metadata || {};
    const snapshot = inputArg.snapshot || snapshotValue(metadata.snapshot);
    const mac = DoorbirdMapper.normalizeMac(inputArg.macAddress || stringMetadata(metadata, 'macAddress') || stringMetadata(metadata, 'macaddress'));
    const hasManualData = Boolean(snapshot || inputArg.rawInfo || inputArg.status || inputArg.client || metadata.client);
    const text = [inputArg.name, inputArg.manufacturer, inputArg.model, metadata.name, metadata.manufacturer, metadata.model].filter(Boolean).join(' ').toLowerCase();
    const matched = Boolean(endpoint.host || metadata.doorbird || hasManualData || mac.startsWith(doorbirdOui) || text.includes('doorbird'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain DoorBird setup hints.' };
    }

    const port = endpoint.port || doorbirdDefaultPort;
    const id = mac || inputArg.id || snapshot?.deviceInfo.id || snapshot?.deviceInfo.macAddress || (endpoint.host ? `${endpoint.host}:${port}` : undefined);
    return {
      matched: true,
      confidence: endpoint.host || hasManualData || mac.startsWith(doorbirdOui) ? 'high' : 'medium',
      reason: endpoint.host ? 'Manual entry contains a local DoorBird HTTP endpoint.' : 'Manual entry contains DoorBird metadata.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: doorbirdDomain,
        id,
        host: endpoint.host,
        port,
        name: inputArg.name || snapshot?.deviceInfo.name || endpoint.host,
        manufacturer: inputArg.manufacturer || snapshot?.deviceInfo.manufacturer || doorbirdManufacturer,
        model: inputArg.model || snapshot?.deviceInfo.model,
        macAddress: mac || snapshot?.deviceInfo.macAddress,
        metadata: {
          ...metadata,
          doorbird: true,
          protocol: endpoint.protocol,
          ssl: endpoint.protocol === 'https',
          url: endpoint.url,
          username: inputArg.username || stringMetadata(metadata, 'username'),
          password: inputArg.password || stringMetadata(metadata, 'password'),
          snapshot,
          rawInfo: inputArg.rawInfo || metadata.rawInfo,
          status: inputArg.status || metadata.status,
          client: inputArg.client || metadata.client,
          commandExecutor: inputArg.commandExecutor || metadata.commandExecutor,
          discoveryProtocol: 'manual',
        },
      },
      metadata: {
        protocol: endpoint.protocol,
        url: endpoint.url,
        manualSupported: true,
      },
    };
  }
}

export class DoorbirdMdnsMatcher implements IDiscoveryMatcher<IDoorbirdMdnsRecord> {
  public id = 'doorbird-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize DoorBird zeroconf records on _axis-video._tcp.local. with DoorBird OUI macaddress.';

  public async matches(recordArg: IDoorbirdMdnsRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const type = normalizeMdnsType(recordArg.type || recordArg.serviceType || '');
    const properties = { ...recordArg.txt, ...recordArg.properties };
    const mac = DoorbirdMapper.normalizeMac(valueForKey(properties, 'macaddress') || valueForKey(properties, 'macAddress') || valueForKey(properties, 'mac'));
    const serviceMatch = type === doorbirdMdnsType;
    const ouiMatch = mac.startsWith(doorbirdOui);
    if (!serviceMatch || !ouiMatch) {
      return { matched: false, confidence: serviceMatch ? 'medium' : 'low', reason: 'mDNS record does not match Home Assistant DoorBird zeroconf criteria.' };
    }

    const ipAddress = recordArg.ipAddress || firstAddress(recordArg.addresses);
    if (ipAddress && (!isIpv4(ipAddress) || isIpv4LinkLocal(ipAddress))) {
      return { matched: false, confidence: 'medium', reason: 'DoorBird zeroconf candidate is not a usable non-link-local IPv4 address.' };
    }

    const host = recordArg.host || ipAddress || recordArg.hostname;
    const port = recordArg.port || numberString(valueForKey(properties, 'port')) || doorbirdDefaultPort;
    const name = cleanMdnsName(recordArg.name || recordArg.hostname || valueForKey(properties, 'name')) || 'DoorBird';
    return {
      matched: true,
      confidence: host ? 'certain' : 'high',
      reason: 'mDNS record matches DoorBird _axis-video zeroconf type and OUI macaddress.',
      normalizedDeviceId: mac,
      candidate: {
        source: 'mdns',
        integrationDomain: doorbirdDomain,
        id: mac,
        host,
        port,
        name,
        manufacturer: doorbirdManufacturer,
        macAddress: mac,
        metadata: {
          doorbird: true,
          mdnsName: recordArg.name,
          mdnsType: type,
          txt: properties,
          macaddress: mac,
          protocol: 'http' satisfies TDoorbirdProtocol,
          ssl: false,
          discoveryProtocol: 'zeroconf',
        },
      },
      metadata: { mdnsType: type, macaddress: mac, zeroconf: true },
    };
  }
}

export class DoorbirdCandidateValidator implements IDiscoveryValidator {
  public id = 'doorbird-candidate-validator';
  public description = 'Validate DoorBird candidates have zeroconf/manual metadata and a local setup source.';

  public async validate(candidateArg: IDiscoveryCandidate, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    if (candidateArg.integrationDomain && candidateArg.integrationDomain !== doorbirdDomain) {
      return { matched: false, confidence: 'low', reason: `Candidate belongs to ${candidateArg.integrationDomain}, not DoorBird.` };
    }
    const metadata = candidateArg.metadata || {};
    const mdnsType = normalizeMdnsType(stringMetadata(metadata, 'mdnsType') || '');
    const mac = DoorbirdMapper.normalizeMac(candidateArg.macAddress || candidateArg.id || stringMetadata(metadata, 'macaddress') || stringMetadata(metadata, 'macAddress'));
    const snapshot = snapshotValue(metadata.snapshot);
    const endpoint = endpointFromCandidate(candidateArg);
    const text = [candidateArg.name, candidateArg.manufacturer, candidateArg.model, metadata.name, metadata.manufacturer, metadata.model].filter(Boolean).join(' ').toLowerCase();
    const matched = candidateArg.integrationDomain === doorbirdDomain
      || Boolean(metadata.doorbird)
      || mdnsType === doorbirdMdnsType && mac.startsWith(doorbirdOui)
      || mac.startsWith(doorbirdOui)
      || text.includes('doorbird')
      || Boolean(snapshot || metadata.client || metadata.rawInfo);
    const hasUsableSource = Boolean(endpoint.host || snapshot || metadata.client || metadata.rawInfo);

    if (!matched || !hasUsableSource) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'DoorBird candidate lacks a usable host, injected client, snapshot, or raw info.' : 'Candidate is not DoorBird.',
        normalizedDeviceId: candidateArg.id || mac || (endpoint.host ? `${endpoint.host}:${endpoint.port || doorbirdDefaultPort}` : undefined),
      };
    }
    if (endpoint.port !== undefined && !isValidPort(endpoint.port)) {
      return { matched: false, confidence: 'low', reason: 'DoorBird candidate has an invalid port.' };
    }

    const id = candidateArg.id || mac || snapshot?.deviceInfo.id || (endpoint.host ? `${endpoint.host}:${endpoint.port || doorbirdDefaultPort}` : undefined);
    return {
      matched: true,
      confidence: id && endpoint.host ? 'certain' : endpoint.host ? 'high' : 'medium',
      reason: 'Candidate has DoorBird metadata and a local endpoint, client, snapshot, or raw info.',
      normalizedDeviceId: id,
      candidate: {
        ...candidateArg,
        integrationDomain: doorbirdDomain,
        id,
        host: endpoint.host,
        port: endpoint.port || doorbirdDefaultPort,
        manufacturer: candidateArg.manufacturer || doorbirdManufacturer,
        macAddress: mac || candidateArg.macAddress,
        metadata: {
          ...metadata,
          doorbird: true,
          protocol: endpoint.protocol,
          ssl: endpoint.protocol === 'https',
          url: endpoint.url,
        },
      },
    };
  }
}

export const createDoorbirdDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: doorbirdDomain, displayName: 'DoorBird' })
    .addMatcher(new DoorbirdManualMatcher())
    .addMatcher(new DoorbirdMdnsMatcher())
    .addValidator(new DoorbirdCandidateValidator());
};

const endpointFromManual = (inputArg: IDoorbirdManualEntry): { protocol: TDoorbirdProtocol; host?: string; port?: number; url?: string } => {
  const parsed = parseEndpoint(inputArg.url || inputArg.host);
  if (parsed) {
    return parsed;
  }
  const protocol: TDoorbirdProtocol = inputArg.protocol || (inputArg.ssl || inputArg.tls ? 'https' : 'http');
  const port = inputArg.port || (protocol === 'https' ? doorbirdDefaultHttpsPort : doorbirdDefaultPort);
  return { protocol, host: inputArg.host, port, url: inputArg.host ? `${protocol}://${inputArg.host}:${port}` : undefined };
};

const endpointFromCandidate = (candidateArg: IDiscoveryCandidate): { protocol: TDoorbirdProtocol; host?: string; port?: number; url?: string } => {
  const metadata = candidateArg.metadata || {};
  const parsed = parseEndpoint(stringMetadata(metadata, 'url') || candidateArg.host);
  if (parsed) {
    return parsed;
  }
  const protocol: TDoorbirdProtocol = stringMetadata(metadata, 'protocol') === 'https' || metadata.ssl === true ? 'https' : 'http';
  const port = candidateArg.port || (protocol === 'https' ? doorbirdDefaultHttpsPort : doorbirdDefaultPort);
  return { protocol, host: candidateArg.host, port, url: candidateArg.host ? `${protocol}://${candidateArg.host}:${port}` : undefined };
};

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

const snapshotValue = (valueArg: unknown) => {
  return valueArg && typeof valueArg === 'object' && 'deviceInfo' in valueArg && 'cameras' in valueArg ? valueArg as import('./doorbird.types.js').IDoorbirdSnapshot : undefined;
};

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

const cleanMdnsName = (valueArg: string | undefined): string | undefined => {
  return valueArg?.replace(/\._axis-video\._tcp\.local\.?$/i, '').replace(/\.local\.?$/i, '').trim() || undefined;
};

const valueForKey = (recordArg: Record<string, string | undefined> | undefined, keyArg: string): string | undefined => {
  if (!recordArg) {
    return undefined;
  }
  const lowerKey = keyArg.toLowerCase();
  for (const [key, value] of Object.entries(recordArg)) {
    if (key.toLowerCase() === lowerKey) {
      return value;
    }
  }
  return undefined;
};

const stringMetadata = (metadataArg: Record<string, unknown>, keyArg: string): string | undefined => {
  const value = metadataArg[keyArg];
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
};

const firstAddress = (addressesArg: string[] | undefined): string | undefined => addressesArg?.find((addressArg) => isIpv4(addressArg) && !isIpv4LinkLocal(addressArg)) || addressesArg?.[0];

const isIpv4 = (valueArg: string): boolean => /^(?:\d{1,3}\.){3}\d{1,3}$/.test(valueArg) && valueArg.split('.').every((partArg) => Number(partArg) >= 0 && Number(partArg) <= 255);

const isIpv4LinkLocal = (valueArg: string): boolean => valueArg.startsWith('169.254.');

const numberString = (valueArg: string | undefined): number | undefined => {
  if (!valueArg) {
    return undefined;
  }
  const value = Number(valueArg);
  return Number.isFinite(value) ? Math.round(value) : undefined;
};

const isValidPort = (valueArg: number): boolean => Number.isInteger(valueArg) && valueArg > 0 && valueArg <= 65535;
