import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import { PrusaLinkMapper } from './prusalink.mapper.js';
import type { IPrusaLinkHttpCandidateRecord, IPrusaLinkManualEntry, IPrusaLinkRawData, IPrusaLinkSnapshot } from './prusalink.types.js';
import { prusaLinkDefaultPort, prusaLinkDefaultUsername, prusaLinkDomain } from './prusalink.types.js';

export class PrusaLinkManualMatcher implements IDiscoveryMatcher<IPrusaLinkManualEntry> {
  public id = 'prusalink-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual PrusaLink local HTTP and snapshot setup entries.';

  public async matches(inputArg: IPrusaLinkManualEntry): Promise<IDiscoveryMatch> {
    const metadata = inputArg.metadata || {};
    const parsed = parseEndpoint(inputArg.url || inputArg.host);
    const snapshot = inputArg.snapshot || snapshotValue(metadata.snapshot);
    const rawData = rawDataValue(inputArg.rawData || metadata.rawData);
    const hasManualData = Boolean(snapshot || rawData || inputArg.version || inputArg.status || inputArg.job || inputArg.info || metadata.client);
    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.prusalink || hasManualData || text.includes('prusalink') || text.includes('prusa'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain PrusaLink setup hints.' };
    }

    const host = parsed?.host || (inputArg.url ? undefined : inputArg.host);
    const port = inputArg.port || parsed?.port || prusaLinkDefaultPort;
    const id = inputArg.id || (snapshot ? PrusaLinkMapper.snapshotId(snapshot) : undefined) || (host ? `${host}:${port}` : undefined);
    return {
      matched: true,
      confidence: host || hasManualData ? 'high' : 'medium',
      reason: 'Manual entry can start PrusaLink setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: prusaLinkDomain,
        id,
        host,
        port,
        name: inputArg.name || snapshot?.printer.name,
        manufacturer: inputArg.manufacturer || snapshot?.printer.manufacturer || 'Prusa',
        model: inputArg.model || snapshot?.printer.model,
        metadata: {
          ...metadata,
          prusalink: true,
          ssl: inputArg.ssl ?? inputArg.tls ?? parsed?.ssl ?? booleanMetadata(metadata, 'ssl') ?? false,
          verifySsl: inputArg.verifySsl ?? metadata.verifySsl,
          username: inputArg.username || stringMetadata(metadata, 'username') || prusaLinkDefaultUsername,
          password: inputArg.password || inputArg.apiKey || stringMetadata(metadata, 'password') || stringMetadata(metadata, 'apiKey'),
          url: inputArg.url || (parsed ? inputArg.host : undefined),
          hasManualData,
          snapshot,
          rawData: rawData || rawDataFromInput(inputArg),
        },
      },
    };
  }
}

export class PrusaLinkHttpMatcher implements IDiscoveryMatcher<IPrusaLinkHttpCandidateRecord> {
  public id = 'prusalink-http-match';
  public source = 'http' as const;
  public description = 'Recognize local HTTP candidates that point at a PrusaLink API.';

  public async matches(recordArg: IPrusaLinkHttpCandidateRecord): Promise<IDiscoveryMatch> {
    const url = recordArg.url || recordArg.location;
    const parsed = parseEndpoint(url);
    const metadata = recordArg.metadata || {};
    const headers = normalizeKeys(recordArg.headers || {});
    const path = parsed?.path || '';
    const text = [url, recordArg.name, recordArg.manufacturer, recordArg.model, headers.server, headers['x-powered-by']].filter(Boolean).join(' ').toLowerCase();
    const matched = Boolean(metadata.prusalink || path.startsWith('/api/version') || path.startsWith('/api/v1/') || text.includes('prusalink') || text.includes('prusa'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'HTTP candidate does not look like a PrusaLink API.' };
    }

    const host = recordArg.host || parsed?.host;
    const port = recordArg.port || parsed?.port || prusaLinkDefaultPort;
    const id = host ? `${host}:${port}` : undefined;
    return {
      matched: true,
      confidence: path.startsWith('/api/') && host ? 'high' : host ? 'medium' : 'low',
      reason: 'HTTP candidate has PrusaLink API hints.',
      normalizedDeviceId: id,
      candidate: {
        source: 'http',
        integrationDomain: prusaLinkDomain,
        id,
        host,
        port,
        name: recordArg.name || 'PrusaLink',
        manufacturer: recordArg.manufacturer || 'Prusa',
        model: recordArg.model,
        metadata: {
          ...metadata,
          prusalink: true,
          ssl: recordArg.ssl ?? parsed?.ssl ?? false,
          url,
          headers,
        },
      },
    };
  }
}

export class PrusaLinkCandidateValidator implements IDiscoveryValidator {
  public id = 'prusalink-candidate-validator';
  public description = 'Validate PrusaLink candidates have a usable HTTP endpoint or snapshot/manual data.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const metadata = candidateArg.metadata || {};
    const text = [candidateArg.integrationDomain, candidateArg.name, candidateArg.manufacturer, candidateArg.model].filter(Boolean).join(' ').toLowerCase();
    const matched = candidateArg.integrationDomain === prusaLinkDomain
      || Boolean(metadata.prusalink)
      || text.includes('prusalink')
      || text.includes('prusa');
    const snapshot = snapshotValue(metadata.snapshot);
    const hasManualData = Boolean(snapshot || metadata.rawData || metadata.client);
    const port = candidateArg.port || prusaLinkDefaultPort;
    const hasUsableAddress = Boolean(candidateArg.host && isValidPort(port));

    if (!matched || (!hasUsableAddress && !hasManualData)) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'PrusaLink candidate lacks a usable host, client, or snapshot/manual data.' : 'Candidate is not PrusaLink.',
        normalizedDeviceId: candidateArg.id || (snapshot ? PrusaLinkMapper.snapshotId(snapshot) : undefined) || (candidateArg.host ? `${candidateArg.host}:${port}` : undefined),
      };
    }

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

export const createPrusaLinkDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: prusaLinkDomain, displayName: 'PrusaLink' })
    .addMatcher(new PrusaLinkManualMatcher())
    .addMatcher(new PrusaLinkHttpMatcher())
    .addValidator(new PrusaLinkCandidateValidator());
};

const parseEndpoint = (valueArg: string | undefined): { host: string; port?: number; ssl: boolean; path: string } | undefined => {
  if (!valueArg || !/^[a-z][a-z0-9+.-]*:\/\//i.test(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 rawDataFromInput = (inputArg: IPrusaLinkManualEntry): Partial<IPrusaLinkRawData> | undefined => {
  const rawData = {
    version: inputArg.version,
    status: inputArg.status,
    legacyStatus: inputArg.legacyStatus,
    job: inputArg.job,
    info: inputArg.info,
  };
  return Object.values(rawData).some(Boolean) ? rawData : undefined;
};

const snapshotValue = (valueArg: unknown): IPrusaLinkSnapshot | undefined => {
  return valueArg && typeof valueArg === 'object' && 'printer' in valueArg && 'telemetry' in valueArg ? valueArg as IPrusaLinkSnapshot : undefined;
};

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

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

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

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