import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IBraviatvManualEntry, IBraviatvMdnsRecord, IBraviatvSsdpRecord } from './braviatv.types.js';

const scalarWebApiService = 'urn:schemas-sony-com:service:ScalarWebAPI:1';

export class BraviatvSsdpMatcher implements IDiscoveryMatcher<IBraviatvSsdpRecord> {
  public id = 'braviatv-ssdp-match';
  public source = 'ssdp' as const;
  public description = 'Recognize Sony Bravia ScalarWebAPI SSDP advertisements.';

  public async matches(recordArg: IBraviatvSsdpRecord): Promise<IDiscoveryMatch> {
    const st = stringValue(recordArg, 'st', 'ST', 'ssdp_st') || recordArg.ssdp_st;
    const usn = stringValue(recordArg, 'usn', 'USN', 'udn', 'UDN', 'ssdp_usn') || recordArg.ssdp_usn;
    const location = stringValue(recordArg, 'location', 'LOCATION', 'ssdp_location') || recordArg.ssdp_location;
    const manufacturer = stringValue(recordArg, 'manufacturer', 'MANUFACTURER', 'upnp:manufacturer') || '';
    const model = stringValue(recordArg, 'modelName', 'model_name', 'model', 'upnp:modelName');
    const friendlyName = stringValue(recordArg, 'friendlyName', 'friendly_name', 'upnp:friendlyName');
    const scalarInfo = scalarWebApiInfo(recordArg);
    const services = scalarWebApiServices(scalarInfo);
    const matchedBySt = st?.toLowerCase() === scalarWebApiService.toLowerCase();
    const matchedByScalarInfo = services.includes('videoScreen') || services.includes('system') || Boolean(scalarBaseUrl(scalarInfo));
    const matchedBySonyRenderer = startsSony(manufacturer) && String(st || '').toLowerCase().includes('mediarenderer');
    if (!matchedBySt && !matchedByScalarInfo && !matchedBySonyRenderer) {
      return { matched: false, confidence: 'low', reason: 'SSDP record is not a Sony Bravia ScalarWebAPI advertisement.' };
    }

    const baseUrl = scalarBaseUrl(scalarInfo);
    const url = safeUrl(baseUrl) || safeUrl(location);
    const id = stripUuid(usn || stringValue(recordArg, 'udn', 'UDN')) || stringValue(recordArg, 'cid', 'macAddr');
    return {
      matched: true,
      confidence: id ? 'certain' : 'high',
      reason: 'SSDP record matches Sony Bravia ScalarWebAPI metadata.',
      normalizedDeviceId: id,
      candidate: {
        source: 'ssdp',
        integrationDomain: 'braviatv',
        id,
        host: url?.hostname,
        port: portFromUrl(url),
        name: friendlyName,
        manufacturer: manufacturer || 'Sony',
        model,
        metadata: {
          st,
          usn,
          location,
          scalarWebApiBaseUrl: baseUrl,
          scalarWebApiServices: services,
        },
      },
    };
  }
}

export class BraviatvMdnsMatcher implements IDiscoveryMatcher<IBraviatvMdnsRecord> {
  public id = 'braviatv-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize Sony Bravia mDNS setup hints.';

  public async matches(recordArg: IBraviatvMdnsRecord): Promise<IDiscoveryMatch> {
    const properties = { ...(recordArg.txt || {}), ...(recordArg.properties || {}) };
    const type = normalizeType(recordArg.type);
    const manufacturer = valueForKey(properties, 'manufacturer') || valueForKey(properties, 'mf') || '';
    const model = valueForKey(properties, 'model') || valueForKey(properties, 'md') || valueForKey(properties, 'modelName');
    const name = cleanName(valueForKey(properties, 'name') || valueForKey(properties, 'fn') || recordArg.name);
    const matched = type.includes('sonyapilib') || type.includes('bravia') || isSonyTvHint(manufacturer, model, name);
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'mDNS record does not contain Sony Bravia hints.' };
    }
    const id = valueForKey(properties, 'cid') || valueForKey(properties, 'id') || valueForKey(properties, 'deviceid') || name;
    return {
      matched: true,
      confidence: id ? 'certain' : recordArg.host ? 'high' : 'medium',
      reason: 'mDNS record matches Sony Bravia metadata.',
      normalizedDeviceId: id,
      candidate: {
        source: 'mdns',
        integrationDomain: 'braviatv',
        id,
        host: recordArg.host,
        port: recordArg.port || 80,
        name,
        manufacturer: manufacturer || 'Sony',
        model,
        macAddress: valueForKey(properties, 'mac') || valueForKey(properties, 'macaddress') || valueForKey(properties, 'deviceid'),
        metadata: {
          mdnsType: recordArg.type,
          mdnsName: recordArg.name,
          txt: properties,
        },
      },
    };
  }
}

export class BraviatvManualMatcher implements IDiscoveryMatcher<IBraviatvManualEntry> {
  public id = 'braviatv-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Sony Bravia TV setup entries.';

  public async matches(inputArg: IBraviatvManualEntry): Promise<IDiscoveryMatch> {
    const matched = Boolean(inputArg.host || inputArg.metadata?.braviatv || inputArg.metadata?.scalarWebApiBaseUrl || isSonyTvHint(inputArg.manufacturer, inputArg.model, inputArg.name));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain Sony Bravia setup hints.' };
    }
    const id = inputArg.cid || inputArg.id || inputArg.macAddress || inputArg.serialNumber;
    return {
      matched: true,
      confidence: inputArg.host ? 'high' : 'medium',
      reason: 'Manual entry can start Sony Bravia setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: 'braviatv',
        id,
        host: inputArg.host,
        port: inputArg.port || (inputArg.useSsl ? 443 : 80),
        name: inputArg.name,
        manufacturer: inputArg.manufacturer || 'Sony',
        model: inputArg.model,
        serialNumber: inputArg.serialNumber,
        macAddress: inputArg.macAddress,
        metadata: {
          ...(inputArg.metadata || {}),
          cid: inputArg.cid,
          psk: inputArg.psk,
          useSsl: inputArg.useSsl,
        },
      },
    };
  }
}

export class BraviatvCandidateValidator implements IDiscoveryValidator {
  public id = 'braviatv-candidate-validator';
  public description = 'Validate Sony Bravia TV discovery candidates.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const matched = candidateArg.integrationDomain === 'braviatv'
      || Boolean(candidateArg.metadata?.braviatv)
      || Boolean(candidateArg.metadata?.scalarWebApiBaseUrl)
      || isSonyTvHint(candidateArg.manufacturer, candidateArg.model, candidateArg.name);
    return {
      matched,
      confidence: matched && candidateArg.host ? 'high' : matched ? 'medium' : 'low',
      reason: matched ? 'Candidate has Sony Bravia metadata.' : 'Candidate is not Sony Bravia TV.',
      candidate: matched ? candidateArg : undefined,
      normalizedDeviceId: candidateArg.id || candidateArg.macAddress || candidateArg.serialNumber || candidateArg.host,
    };
  }
}

export const createBraviatvDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: 'braviatv', displayName: 'Sony Bravia TV' })
    .addMatcher(new BraviatvSsdpMatcher())
    .addMatcher(new BraviatvMdnsMatcher())
    .addMatcher(new BraviatvManualMatcher())
    .addValidator(new BraviatvCandidateValidator());
};

const startsSony = (valueArg: string | undefined): boolean => Boolean(valueArg?.toLowerCase().startsWith('sony'));

const isSonyTvHint = (...valuesArg: Array<string | undefined>): boolean => {
  const value = valuesArg.filter(Boolean).join(' ').toLowerCase();
  return value.includes('sony') || value.includes('bravia');
};

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

const cleanName = (valueArg?: string): string | undefined => valueArg?.replace(/\._[^.]+\._tcp\.local\.?$/i, '').replace(/\.local\.?$/i, '').trim() || undefined;

const stripUuid = (valueArg?: string): string | undefined => {
  if (!valueArg) {
    return undefined;
  }
  return valueArg.replace(/^uuid:/i, '').split('::')[0];
};

const safeUrl = (valueArg?: string): URL | undefined => {
  if (!valueArg) {
    return undefined;
  }
  try {
    return new URL(valueArg);
  } catch {
    return undefined;
  }
};

const portFromUrl = (urlArg: URL | undefined): number | undefined => {
  if (!urlArg?.port) {
    return undefined;
  }
  const port = Number(urlArg.port);
  return Number.isFinite(port) ? port : 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 && value) {
      return value;
    }
  }
  return undefined;
};

const stringValue = (recordArg: IBraviatvSsdpRecord, ...keysArg: string[]): string | undefined => {
  const maps = [recordArg.headers, recordArg.upnp, recordArg as Record<string, unknown>].filter(Boolean) as Array<Record<string, unknown>>;
  for (const key of keysArg) {
    const lowerKey = key.toLowerCase();
    for (const map of maps) {
      for (const [candidateKey, value] of Object.entries(map)) {
        if (candidateKey.toLowerCase() === lowerKey && typeof value === 'string' && value) {
          return value;
        }
      }
    }
  }
  return undefined;
};

const scalarWebApiInfo = (recordArg: IBraviatvSsdpRecord): Record<string, unknown> | undefined => {
  const value = recordArg.upnp?.X_ScalarWebAPI_DeviceInfo || recordArg.upnp?.['x_scalarwebapi_deviceinfo'] || recordArg.X_ScalarWebAPI_DeviceInfo;
  return value && typeof value === 'object' ? value as Record<string, unknown> : undefined;
};

const scalarBaseUrl = (infoArg: Record<string, unknown> | undefined): string | undefined => {
  const value = infoArg?.X_ScalarWebAPI_BaseURL || infoArg?.['x_scalarwebapi_baseurl'];
  return typeof value === 'string' ? value : undefined;
};

const scalarWebApiServices = (infoArg: Record<string, unknown> | undefined): string[] => {
  const serviceList = infoArg?.X_ScalarWebAPI_ServiceList || infoArg?.['x_scalarwebapi_servicelist'];
  if (!serviceList || typeof serviceList !== 'object') {
    return [];
  }
  const serviceTypes = (serviceList as Record<string, unknown>).X_ScalarWebAPI_ServiceType || (serviceList as Record<string, unknown>)['x_scalarwebapi_servicetype'];
  if (Array.isArray(serviceTypes)) {
    return serviceTypes.filter((itemArg): itemArg is string => typeof itemArg === 'string');
  }
  if (typeof serviceTypes === 'string') {
    return [serviceTypes];
  }
  return [];
};
