import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryContext, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import { SynologyDsmMapper } from './synology_dsm.mapper.js';
import type { ISynologyDsmHttpDiscoveryRecord, ISynologyDsmManualDiscoveryRecord, ISynologyDsmMdnsDiscoveryRecord, ISynologyDsmSnapshot, ISynologyDsmSsdpDiscoveryRecord } from './synology_dsm.types.js';
import { synologyDsmDefaultSsl, synologyDsmDomain } from './synology_dsm.types.js';

const synologyTextHints = ['synology', 'diskstation', 'rackstation', 'dsm', 'surveillance station'];
const synologyMdnsType = '_http._tcp.local.';
const synologySsdpDeviceType = 'urn:schemas-upnp-org:device:Basic:1';

export class SynologyDsmManualMatcher implements IDiscoveryMatcher<ISynologyDsmManualDiscoveryRecord> {
  public id = 'synology-dsm-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Synology DSM local NAS setup entries and snapshot-only records.';

  public async matches(inputArg: ISynologyDsmManualDiscoveryRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = inputArg.metadata || {};
    const parsedUrl = parseUrl(inputArg.url);
    const snapshot = inputArg.snapshot || metadata.snapshot as ISynologyDsmSnapshot | undefined;
    const host = inputArg.host || parsedUrl?.host || snapshot?.system.host;
    const text = textValue(inputArg.integrationDomain, inputArg.manufacturer, inputArg.model, inputArg.name, metadata.manufacturer, metadata.model, metadata.name, snapshot?.system.name, snapshot?.system.model);
    const matched = inputArg.integrationDomain === synologyDsmDomain
      || metadata.synologyDsm === true
      || Boolean(snapshot)
      || Boolean(host)
      || synologyTextHints.some((hintArg) => text.includes(hintArg));

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

    const ssl = booleanValue(inputArg.ssl) ?? parsedUrl?.ssl ?? booleanValue(metadata.ssl) ?? snapshot?.system.ssl ?? synologyDsmDefaultSsl;
    const port = inputArg.port || parsedUrl?.port || snapshot?.system.port || SynologyDsmMapper.defaultPort(ssl);
    const serial = inputArg.serialNumber || snapshot?.system.serial;
    const mac = SynologyDsmMapper.normalizeMac(inputArg.macAddress || snapshot?.system.macs?.[0]);
    const id = inputArg.id || serial || mac || snapshot?.system.id || (host ? `${host}:${port}` : undefined);
    return {
      matched: true,
      confidence: snapshot || serial || mac ? 'certain' : host ? 'high' : 'medium',
      reason: snapshot ? 'Manual entry includes a Synology DSM snapshot.' : 'Manual entry can start Synology DSM setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: synologyDsmDomain,
        id,
        host,
        port,
        name: inputArg.name || snapshot?.system.name || snapshot?.system.hostname || host || 'Synology DSM',
        manufacturer: inputArg.manufacturer || 'Synology',
        model: inputArg.model || snapshot?.system.model || 'DSM NAS',
        serialNumber: serial,
        macAddress: mac,
        metadata: {
          ...metadata,
          synologyDsm: true,
          ssl,
          verifySsl: inputArg.verifySsl ?? metadata.verifySsl,
          username: inputArg.username ?? metadata.username,
          password: inputArg.password ?? metadata.password,
          snapshot,
          hasSnapshot: Boolean(snapshot),
          upstreamSupportsSsdp: true,
          upstreamSupportsZeroconf: true,
          liveHttpImplemented: false,
        },
      },
      metadata: { ssl, hasSnapshot: Boolean(snapshot), upstreamSupportsSsdp: true, upstreamSupportsZeroconf: true, liveHttpImplemented: false },
    };
  }
}

export class SynologyDsmHttpMatcher implements IDiscoveryMatcher<ISynologyDsmHttpDiscoveryRecord> {
  public id = 'synology-dsm-http-match';
  public source = 'http' as const;
  public description = 'Recognize local HTTP candidates that look like DSM web/API endpoints.';

  public async matches(inputArg: ISynologyDsmHttpDiscoveryRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = inputArg.metadata || {};
    const url = inputArg.url || inputArg.location;
    const parsedUrl = parseUrl(url);
    const headers = normalizeKeys(inputArg.headers || {});
    const host = inputArg.host || parsedUrl?.host;
    const port = inputArg.port || parsedUrl?.port;
    const text = textValue(url, inputArg.name, inputArg.manufacturer, inputArg.model, headers.server, headers['x-powered-by'], metadata.name, metadata.manufacturer, metadata.model);
    const matched = inputArg.ssl !== undefined
      || metadata.synologyDsm === true
      || port === 5000
      || port === 5001
      || Boolean(parsedUrl?.path?.includes('/webapi/'))
      || synologyTextHints.some((hintArg) => text.includes(hintArg));

    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'HTTP candidate does not look like a Synology DSM endpoint.' };
    }

    const ssl = inputArg.ssl ?? parsedUrl?.ssl ?? booleanValue(metadata.ssl) ?? (port === 5000 ? false : synologyDsmDefaultSsl);
    const resolvedPort = port || SynologyDsmMapper.defaultPort(ssl);
    const id = host ? `${host}:${resolvedPort}` : undefined;
    return {
      matched: true,
      confidence: host && (parsedUrl?.path?.includes('/webapi/') || port === 5000 || port === 5001) ? 'high' : host ? 'medium' : 'low',
      reason: 'HTTP candidate has Synology DSM endpoint hints.',
      normalizedDeviceId: id,
      candidate: {
        source: 'http',
        integrationDomain: synologyDsmDomain,
        id,
        host,
        port: resolvedPort,
        name: inputArg.name || 'Synology DSM',
        manufacturer: inputArg.manufacturer || 'Synology',
        model: inputArg.model || 'DSM NAS',
        metadata: {
          ...metadata,
          synologyDsm: true,
          ssl,
          url,
          headers,
          upstreamSupportsSsdp: true,
          upstreamSupportsZeroconf: true,
          liveHttpImplemented: false,
        },
      },
      metadata: { ssl, upstreamSupportsSsdp: true, upstreamSupportsZeroconf: true, liveHttpImplemented: false },
    };
  }
}

export class SynologyDsmSsdpMatcher implements IDiscoveryMatcher<ISynologyDsmSsdpDiscoveryRecord> {
  public id = 'synology-dsm-ssdp-match';
  public source = 'ssdp' as const;
  public description = 'Recognize Home Assistant supported Synology SSDP advertisements.';

  public async matches(inputArg: ISynologyDsmSsdpDiscoveryRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = inputArg.metadata || {};
    const upnp = inputArg.upnp || {};
    const location = stringValue(inputArg.ssdpLocation || inputArg.ssdp_location || inputArg.location || metadata.ssdpLocation || metadata.location);
    const host = inputArg.host || hostFromUrl(location);
    const st = stringValue(inputArg.st || metadata.st || metadata.ssdpSt);
    const friendlyName = firstString(upnp.friendlyName, upnp.FriendlyName, upnp.friendly_name, upnp['upnp:ATTR_UPNP_FRIENDLY_NAME'], metadata.friendlyName, inputArg.name);
    const modelName = firstString(upnp.modelName, upnp.ModelName, upnp.model_name, metadata.modelName, inputArg.model);
    const manufacturerName = firstString(upnp.manufacturer, upnp.Manufacturer, metadata.manufacturer, inputArg.manufacturer);
    const serial = firstString(upnp.serialNumber, upnp.SerialNumber, upnp.serial, upnp['upnp:ATTR_UPNP_SERIAL'], metadata.serialNumber, inputArg.serialNumber);
    const text = textValue(inputArg.manufacturer, inputArg.model, inputArg.name, friendlyName, modelName, manufacturerName, st);
    const matched = inputArg.manufacturer === 'Synology'
      || manufacturerName?.toLowerCase() === 'synology'
      || metadata.synologyDsm === true
      || st === synologySsdpDeviceType
      || synologyTextHints.some((hintArg) => text.includes(hintArg));

    if (!matched || !host) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'Synology SSDP advertisement lacks a usable host.' : 'SSDP advertisement is not Synology DSM.',
      };
    }

    const ssl = booleanValue(metadata.ssl) ?? synologyDsmDefaultSsl;
    const port = inputArg.port || portFromUrl(location) || SynologyDsmMapper.defaultPort(ssl);
    const mac = SynologyDsmMapper.normalizeMac(serial);
    const id = inputArg.serialNumber || mac || inputArg.usn || `${host}:${port}`;
    return {
      matched: true,
      confidence: manufacturerName?.toLowerCase() === 'synology' || serial ? 'certain' : 'high',
      reason: 'SSDP advertisement matches Synology DSM support from Home Assistant.',
      normalizedDeviceId: id,
      candidate: {
        source: 'ssdp',
        integrationDomain: synologyDsmDomain,
        id,
        host,
        port,
        name: inputArg.name || friendlyName?.split('(', 1)[0]?.trim() || modelName || 'Synology DSM',
        manufacturer: inputArg.manufacturer || manufacturerName || 'Synology',
        model: inputArg.model || modelName || 'DSM NAS',
        serialNumber: inputArg.serialNumber || serial,
        macAddress: mac,
        metadata: {
          ...metadata,
          synologyDsm: true,
          ssl,
          ssdpSt: st,
          ssdpLocation: location,
          upnp,
          upstreamSupportsSsdp: true,
          upstreamSupportsZeroconf: true,
          liveHttpImplemented: false,
        },
      },
      metadata: { ssl, upstreamSupportsSsdp: true, upstreamSupportsZeroconf: true, liveHttpImplemented: false },
    };
  }
}

export class SynologyDsmMdnsMatcher implements IDiscoveryMatcher<ISynologyDsmMdnsDiscoveryRecord> {
  public id = 'synology-dsm-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize Home Assistant supported Synology zeroconf/mDNS HTTP advertisements.';

  public async matches(inputArg: ISynologyDsmMdnsDiscoveryRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = inputArg.metadata || {};
    const properties = { ...(inputArg.properties || {}), ...(inputArg.txt || {}) };
    const type = stringValue(inputArg.type || inputArg.serviceType || metadata.serviceType);
    const vendor = stringValue(properties.vendor || metadata.vendor);
    const macs = stringValue(properties.mac_address || properties.macAddress || metadata.macAddress)?.split('|').map((valueArg) => SynologyDsmMapper.normalizeMac(valueArg)).filter((valueArg): valueArg is string => Boolean(valueArg)) || [];
    const text = textValue(inputArg.name, inputArg.fullname, inputArg.manufacturer, inputArg.model, vendor, metadata.name, metadata.manufacturer, metadata.model);
    const matched = metadata.synologyDsm === true
      || vendor?.toLowerCase().startsWith('synology')
      || type === synologyMdnsType && synologyTextHints.some((hintArg) => text.includes(hintArg));

    if (!matched || !inputArg.host) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'Synology mDNS advertisement lacks a usable host.' : 'mDNS advertisement is not Synology DSM.',
      };
    }

    const ssl = booleanValue(metadata.ssl) ?? synologyDsmDefaultSsl;
    const port = inputArg.port || SynologyDsmMapper.defaultPort(ssl);
    const name = inputArg.name?.replace(/\._http\._tcp\.local\.?$/i, '') || inputArg.fullname?.replace(/\._http\._tcp\.local\.?$/i, '') || 'Synology DSM';
    const id = inputArg.serialNumber || macs[0] || `${inputArg.host}:${port}`;
    return {
      matched: true,
      confidence: macs.length ? 'certain' : 'high',
      reason: 'mDNS/zeroconf advertisement contains Synology vendor metadata.',
      normalizedDeviceId: id,
      candidate: {
        source: 'mdns',
        integrationDomain: synologyDsmDomain,
        id,
        host: inputArg.host,
        port,
        name,
        manufacturer: inputArg.manufacturer || 'Synology',
        model: inputArg.model || 'DSM NAS',
        serialNumber: inputArg.serialNumber,
        macAddress: inputArg.macAddress || macs[0],
        metadata: {
          ...metadata,
          synologyDsm: true,
          ssl,
          vendor,
          macs,
          properties,
          upstreamSupportsSsdp: true,
          upstreamSupportsZeroconf: true,
          liveHttpImplemented: false,
        },
      },
      metadata: { ssl, upstreamSupportsSsdp: true, upstreamSupportsZeroconf: true, liveHttpImplemented: false },
    };
  }
}

export class SynologyDsmCandidateValidator implements IDiscoveryValidator {
  public id = 'synology-dsm-candidate-validator';
  public description = 'Validate Synology DSM candidates have a local host or snapshot plus Synology identity metadata.';

  public async validate(candidateArg: IDiscoveryCandidate, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = candidateArg.metadata || {};
    const snapshot = metadata.snapshot as ISynologyDsmSnapshot | undefined;
    const text = textValue(candidateArg.integrationDomain, candidateArg.manufacturer, candidateArg.model, candidateArg.name, metadata.manufacturer, metadata.model, metadata.name, metadata.vendor, metadata.ssdpSt);
    const matched = candidateArg.integrationDomain === synologyDsmDomain
      || metadata.synologyDsm === true
      || Boolean(snapshot)
      || candidateArg.port === 5000
      || candidateArg.port === 5001
      || synologyTextHints.some((hintArg) => text.includes(hintArg));
    const hasSource = Boolean(candidateArg.host || snapshot);

    if (!matched || !hasSource) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'Synology DSM candidate lacks host or snapshot information.' : 'Candidate is not Synology DSM.',
      };
    }

    const ssl = booleanValue(metadata.ssl) ?? snapshot?.system.ssl ?? synologyDsmDefaultSsl;
    const port = candidateArg.port || snapshot?.system.port || SynologyDsmMapper.defaultPort(ssl);
    const mac = SynologyDsmMapper.normalizeMac(candidateArg.macAddress || snapshot?.system.macs?.[0]);
    const normalizedDeviceId = candidateArg.id || snapshot?.system.serial || candidateArg.serialNumber || mac || (candidateArg.host ? `${candidateArg.host}:${port}` : snapshot?.system.id);
    return {
      matched: true,
      confidence: snapshot || candidateArg.serialNumber || mac ? 'certain' : candidateArg.host ? 'high' : 'medium',
      reason: 'Candidate has Synology DSM metadata and a usable local source.',
      normalizedDeviceId,
      candidate: {
        ...candidateArg,
        id: candidateArg.id || normalizedDeviceId,
        integrationDomain: synologyDsmDomain,
        port,
        macAddress: mac || candidateArg.macAddress,
        metadata: {
          ...metadata,
          synologyDsm: true,
          ssl,
          upstreamSupportsSsdp: true,
          upstreamSupportsZeroconf: true,
          liveHttpImplemented: false,
        },
      },
      metadata: { ssl, upstreamSupportsSsdp: true, upstreamSupportsZeroconf: true, liveHttpImplemented: false },
    };
  }
}

export const createSynologyDsmDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: synologyDsmDomain, displayName: 'Synology DSM' })
    .addMatcher(new SynologyDsmManualMatcher())
    .addMatcher(new SynologyDsmHttpMatcher())
    .addMatcher(new SynologyDsmSsdpMatcher())
    .addMatcher(new SynologyDsmMdnsMatcher())
    .addValidator(new SynologyDsmCandidateValidator());
};

const parseUrl = (valueArg: string | undefined): { host: string; port?: number; ssl: boolean; path: string } | undefined => {
  if (!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 hostFromUrl = (valueArg: string | undefined): string | undefined => parseUrl(valueArg)?.host;
const portFromUrl = (valueArg: string | undefined): number | undefined => parseUrl(valueArg)?.port;

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 firstString = (...valuesArg: unknown[]): string | undefined => valuesArg.find((valueArg): valueArg is string => typeof valueArg === 'string' && Boolean(valueArg.trim()))?.trim();

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

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

const textValue = (...valuesArg: unknown[]): string => valuesArg.filter((valueArg): valueArg is string => typeof valueArg === 'string').join(' ').toLowerCase();
