import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IPiHoleHttpCandidateRecord, IPiHoleManualEntry, IPiHoleSnapshot } from './pi_hole.types.js';
import { piHoleDefaultLocation, piHoleDefaultName, piHoleDefaultPort, piHoleDomain } from './pi_hole.types.js';

export class PiHoleManualMatcher implements IDiscoveryMatcher<IPiHoleManualEntry> {
  public id = 'pi-hole-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Pi-hole local HTTP and snapshot setup entries.';

  public async matches(inputArg: IPiHoleManualEntry): Promise<IDiscoveryMatch> {
    const parsedUrl = parseUrl(inputArg.url);
    const metadata = inputArg.metadata || {};
    const hasManualData = Boolean(inputArg.snapshot || inputArg.rawData || metadata.snapshot || metadata.rawData || inputArg.statistics || inputArg.status || inputArg.versions);
    const matched = isPiHoleHint(inputArg) || Boolean(inputArg.host || parsedUrl || hasManualData);
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain Pi-hole setup hints.' };
    }

    const host = inputArg.host || parsedUrl?.host;
    const ssl = inputArg.ssl ?? parsedUrl?.ssl ?? booleanMetadata(metadata.ssl) ?? false;
    const port = inputArg.port || parsedUrl?.port || piHoleDefaultPort;
    const id = inputArg.id || snapshotId(inputArg.snapshot || metadata.snapshot) || (host ? `${host}:${port}` : undefined);
    return {
      matched: true,
      confidence: host || hasManualData ? 'high' : 'medium',
      reason: 'Manual entry can start Pi-hole setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: piHoleDomain,
        id,
        host,
        port,
        name: inputArg.name || piHoleDefaultName,
        manufacturer: 'Pi-hole',
        model: inputArg.model || 'Pi-hole',
        metadata: {
          ...metadata,
          piHole: true,
          ssl,
          verifySsl: inputArg.verifySsl ?? metadata.verifySsl,
          location: inputArg.location || parsedUrl?.location || metadata.location || piHoleDefaultLocation,
          apiKey: inputArg.apiKey ?? metadata.apiKey,
          password: inputArg.password ?? metadata.password,
          apiVersion: inputArg.apiVersion || metadata.apiVersion || parsedUrl?.apiVersion,
          url: inputArg.url,
          snapshot: inputArg.snapshot || metadata.snapshot,
          rawData: inputArg.rawData || metadata.rawData,
          hasManualData,
        },
      },
    };
  }
}

export class PiHoleHttpMatcher implements IDiscoveryMatcher<IPiHoleHttpCandidateRecord> {
  public id = 'pi-hole-http-match';
  public source = 'http' as const;
  public description = 'Recognize local HTTP candidates that point at a Pi-hole API.';

  public async matches(recordArg: IPiHoleHttpCandidateRecord): Promise<IDiscoveryMatch> {
    const url = recordArg.url || recordArg.location;
    const parsedUrl = parseUrl(url);
    const headers = normalizeKeys(recordArg.headers || {});
    const metadata = recordArg.metadata || {};
    const text = [url, recordArg.name, recordArg.manufacturer, recordArg.model, headers.server, headers['x-powered-by']].filter(Boolean).join(' ').toLowerCase();
    const matched = Boolean(parsedUrl?.apiVersion || parsedUrl?.location === piHoleDefaultLocation || metadata.piHole || metadata.pi_hole || metadata.pihole || text.includes('pi-hole') || text.includes('pihole'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'HTTP candidate does not look like a Pi-hole API.' };
    }
    const host = recordArg.host || parsedUrl?.host;
    const ssl = recordArg.ssl ?? parsedUrl?.ssl ?? false;
    const port = recordArg.port || parsedUrl?.port || piHoleDefaultPort;
    const id = host ? `${host}:${port}` : undefined;
    return {
      matched: true,
      confidence: parsedUrl?.apiVersion && host ? 'high' : host ? 'medium' : 'low',
      reason: 'HTTP candidate has Pi-hole API hints.',
      normalizedDeviceId: id,
      candidate: {
        source: 'http',
        integrationDomain: piHoleDomain,
        id,
        host,
        port,
        name: recordArg.name || piHoleDefaultName,
        manufacturer: recordArg.manufacturer || 'Pi-hole',
        model: recordArg.model || 'Pi-hole',
        metadata: {
          ...metadata,
          piHole: true,
          ssl,
          url,
          location: parsedUrl?.location || metadata.location || piHoleDefaultLocation,
          apiVersion: parsedUrl?.apiVersion || metadata.apiVersion,
          headers,
        },
      },
    };
  }
}

export class PiHoleCandidateValidator implements IDiscoveryValidator {
  public id = 'pi-hole-candidate-validator';
  public description = 'Validate Pi-hole candidates have a usable HTTP endpoint or snapshot/manual data.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const metadata = candidateArg.metadata || {};
    const matched = candidateArg.integrationDomain === piHoleDomain || isPiHoleHint(candidateArg);
    const hasManualData = Boolean(metadata.snapshot || metadata.rawData || metadata.statistics || metadata.status || metadata.versions);
    const port = candidateArg.port || piHoleDefaultPort;
    const hasUsableAddress = Boolean(candidateArg.host && isValidPort(port));
    if (!matched || (!hasUsableAddress && !hasManualData)) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'Pi-hole candidate lacks a usable host or snapshot/manual data.' : 'Candidate is not Pi-hole.',
        normalizedDeviceId: candidateArg.id || (candidateArg.host ? `${candidateArg.host}:${port}` : undefined),
      };
    }

    const normalizedDeviceId = candidateArg.id || snapshotId(metadata.snapshot) || (candidateArg.host ? `${candidateArg.host}:${port}` : undefined);
    return {
      matched: true,
      confidence: normalizedDeviceId && hasUsableAddress ? 'certain' : hasUsableAddress ? 'high' : 'medium',
      reason: 'Candidate has Pi-hole metadata and a usable HTTP endpoint or snapshot/manual data.',
      normalizedDeviceId,
      candidate: {
        ...candidateArg,
        integrationDomain: piHoleDomain,
        port,
        manufacturer: candidateArg.manufacturer || 'Pi-hole',
        model: candidateArg.model || 'Pi-hole',
      },
    };
  }
}

export const createPiHoleDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: piHoleDomain, displayName: 'Pi-hole' })
    .addMatcher(new PiHoleManualMatcher())
    .addMatcher(new PiHoleHttpMatcher())
    .addValidator(new PiHoleCandidateValidator());
};

const parseUrl = (valueArg: string | undefined): { host: string; port?: number; ssl: boolean; location?: string; apiVersion?: 5 | 6 } | undefined => {
  if (!valueArg) {
    return undefined;
  }
  try {
    const url = new URL(valueArg);
    const path = url.pathname.toLowerCase();
    const apiVersion = path.includes('/api.php') ? 5 : path.startsWith('/api/') || path === '/api' ? 6 : undefined;
    const parts = url.pathname.split('/').filter(Boolean);
    return {
      host: url.hostname,
      port: url.port ? Number(url.port) : undefined,
      ssl: url.protocol === 'https:',
      location: parts[0] && parts[0] !== 'api' ? parts[0] : undefined,
      apiVersion,
    };
  } catch {
    return undefined;
  }
};

const isPiHoleHint = (valueArg: { integrationDomain?: string; manufacturer?: string; model?: string; name?: string; metadata?: Record<string, unknown> }): boolean => {
  const text = [valueArg.integrationDomain, valueArg.manufacturer, valueArg.model, valueArg.name].filter(Boolean).join(' ').toLowerCase();
  return valueArg.integrationDomain === piHoleDomain
    || text.includes('pi-hole')
    || text.includes('pihole')
    || Boolean(valueArg.metadata?.piHole || valueArg.metadata?.pi_hole || valueArg.metadata?.pihole);
};

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 isValidPort = (valueArg: number): boolean => Number.isInteger(valueArg) && valueArg > 0 && valueArg <= 65535;

const booleanMetadata = (valueArg: unknown): boolean | undefined => typeof valueArg === 'boolean' ? valueArg : undefined;

const isPiHoleSnapshot = (valueArg: unknown): valueArg is IPiHoleSnapshot => Boolean(valueArg && typeof valueArg === 'object' && 'statistics' in valueArg && 'status' in valueArg);

const snapshotId = (valueArg: unknown): string | undefined => {
  const snapshot = isPiHoleSnapshot(valueArg) ? valueArg : undefined;
  return snapshot?.uniqueId || snapshot?.host;
};
