import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryContext, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IMotionEyeManualEntry, IMotionEyeUrlRecord, TMotionEyeProtocol } from './motioneye.types.js';
import { motionEyeDefaultPort } from './motioneye.types.js';

export class MotionEyeManualMatcher implements IDiscoveryMatcher<IMotionEyeManualEntry> {
  public id = 'motioneye-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual motionEye base URL or host entries.';

  public async matches(inputArg: IMotionEyeManualEntry, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const endpoint = endpointFromInput(inputArg);
    const hint = hasMotionEyeHint(inputArg.name, inputArg.manufacturer, inputArg.model) || Boolean(inputArg.metadata?.motioneye);
    if (!endpoint.host && !hint) {
      return { matched: false, confidence: 'low', reason: 'Manual motionEye entry requires a URL, host, or motionEye metadata.' };
    }
    const normalizedDeviceId = inputArg.id || endpoint.host || endpoint.url;
    return {
      matched: true,
      confidence: endpoint.host ? 'high' : 'medium',
      reason: endpoint.host ? 'Manual entry contains a local motionEye endpoint.' : 'Manual entry contains motionEye metadata.',
      normalizedDeviceId,
      candidate: {
        source: 'manual',
        integrationDomain: 'motioneye',
        id: normalizedDeviceId,
        host: endpoint.host,
        port: endpoint.port,
        name: inputArg.name || endpoint.host,
        manufacturer: inputArg.manufacturer || 'motionEye',
        model: inputArg.model,
        metadata: {
          ...inputArg.metadata,
          protocol: endpoint.protocol,
          url: endpoint.url,
          adminUsername: inputArg.adminUsername,
          adminPassword: inputArg.adminPassword,
          surveillanceUsername: inputArg.surveillanceUsername,
          surveillancePassword: inputArg.surveillancePassword,
          discoveryProtocol: 'manual',
        },
      },
      metadata: {
        protocol: endpoint.protocol,
        url: endpoint.url,
      },
    };
  }
}

export class MotionEyeUrlMatcher implements IDiscoveryMatcher<IMotionEyeUrlRecord> {
  public id = 'motioneye-url-match';
  public source = 'http' as const;
  public description = 'Recognize local HTTP URL candidates that point at motionEye.';

  public async matches(recordArg: IMotionEyeUrlRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const endpoint = endpointFromInput(recordArg);
    const urlText = recordArg.url || String(recordArg.metadata?.url || '');
    const hint = hasMotionEyeHint(recordArg.name, recordArg.manufacturer, recordArg.model, urlText) || Boolean(recordArg.metadata?.motioneye);
    const defaultPortHint = endpoint.port === motionEyeDefaultPort;
    if (!endpoint.host || (!hint && !defaultPortHint)) {
      return { matched: false, confidence: 'low', reason: 'HTTP candidate does not look like a motionEye endpoint.' };
    }
    return {
      matched: true,
      confidence: hint ? 'high' : 'medium',
      reason: hint ? 'HTTP candidate contains motionEye metadata.' : 'HTTP candidate uses the default motionEye port.',
      normalizedDeviceId: endpoint.host,
      candidate: {
        source: 'http',
        integrationDomain: 'motioneye',
        id: endpoint.host,
        host: endpoint.host,
        port: endpoint.port,
        name: recordArg.name || endpoint.host,
        manufacturer: recordArg.manufacturer || 'motionEye',
        model: recordArg.model,
        metadata: {
          ...recordArg.metadata,
          protocol: endpoint.protocol,
          url: endpoint.url,
          discoveryProtocol: 'http',
        },
      },
    };
  }
}

export class MotionEyeCandidateValidator implements IDiscoveryValidator {
  public id = 'motioneye-candidate-validator';
  public description = 'Validate that a discovery candidate can be configured as a local motionEye server.';

  public async validate(candidateArg: IDiscoveryCandidate, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    if (candidateArg.integrationDomain && candidateArg.integrationDomain !== 'motioneye') {
      return { matched: false, confidence: 'low', reason: `Candidate belongs to ${candidateArg.integrationDomain}, not motionEye.` };
    }
    const endpoint = endpointFromCandidate(candidateArg);
    if (!endpoint.host) {
      return { matched: false, confidence: 'low', reason: 'motionEye candidates require a local URL or host.' };
    }
    if (!Number.isInteger(endpoint.port) || endpoint.port < 1 || endpoint.port > 65535) {
      return { matched: false, confidence: 'low', reason: 'motionEye candidate has an invalid port.' };
    }
    const hasHint = candidateArg.integrationDomain === 'motioneye'
      || candidateArg.source === 'manual'
      || candidateArg.source === 'http'
      || endpoint.port === motionEyeDefaultPort
      || hasMotionEyeHint(candidateArg.name, candidateArg.manufacturer, candidateArg.model)
      || Boolean(candidateArg.metadata?.motioneye);
    if (!hasHint) {
      return { matched: false, confidence: 'low', reason: 'Candidate does not contain motionEye metadata.' };
    }
    return {
      matched: true,
      confidence: candidateArg.integrationDomain === 'motioneye' || candidateArg.source === 'manual' ? 'high' : 'medium',
      reason: 'Candidate has enough local motionEye metadata to start configuration.',
      normalizedDeviceId: candidateArg.id || endpoint.host,
      candidate: {
        ...candidateArg,
        integrationDomain: 'motioneye',
        id: candidateArg.id || endpoint.host,
        host: endpoint.host,
        port: endpoint.port,
        manufacturer: candidateArg.manufacturer || 'motionEye',
        metadata: {
          ...candidateArg.metadata,
          protocol: endpoint.protocol,
          url: endpoint.url,
        },
      },
      metadata: {
        manualSupported: candidateArg.source === 'manual',
        protocol: endpoint.protocol,
        url: endpoint.url,
      },
    };
  }
}

export const createMotionEyeDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: 'motioneye', displayName: 'motionEye' })
    .addMatcher(new MotionEyeManualMatcher())
    .addMatcher(new MotionEyeUrlMatcher())
    .addValidator(new MotionEyeCandidateValidator());
};

const endpointFromInput = (inputArg: IMotionEyeManualEntry | IMotionEyeUrlRecord): { protocol: TMotionEyeProtocol; host?: string; port: number; url?: string } => {
  const metadataUrl = typeof inputArg.metadata?.url === 'string' ? inputArg.metadata.url : undefined;
  const url = safeUrl(inputArg.url || metadataUrl || inputArg.host);
  if (url) {
    const protocol = url.protocol === 'https:' ? 'https' : 'http';
    const port = url.port ? Number(url.port) : protocol === 'https' ? 443 : motionEyeDefaultPort;
    const pathname = url.pathname && url.pathname !== '/' ? url.pathname.replace(/\/$/, '') : '';
    return { protocol, host: url.hostname, port, url: `${protocol}://${url.hostname}:${port}${pathname}` };
  }
  const protocol = ('protocol' in inputArg && inputArg.protocol) || 'http';
  const port = inputArg.port || motionEyeDefaultPort;
  return { protocol, host: inputArg.host, port, url: inputArg.host ? `${protocol}://${inputArg.host}:${port}` : undefined };
};

const endpointFromCandidate = (candidateArg: IDiscoveryCandidate): { protocol: TMotionEyeProtocol; host?: string; port: number; url?: string } => {
  const metadataUrl = typeof candidateArg.metadata?.url === 'string' ? candidateArg.metadata.url : undefined;
  const metadataProtocol = candidateArg.metadata?.protocol === 'https' ? 'https' : 'http';
  const url = safeUrl(metadataUrl || candidateArg.host);
  if (url) {
    const protocol = url.protocol === 'https:' ? 'https' : 'http';
    const port = url.port ? Number(url.port) : protocol === 'https' ? 443 : motionEyeDefaultPort;
    const pathname = url.pathname && url.pathname !== '/' ? url.pathname.replace(/\/$/, '') : '';
    return { protocol, host: url.hostname, port, url: `${protocol}://${url.hostname}:${port}${pathname}` };
  }
  const port = candidateArg.port || motionEyeDefaultPort;
  return { protocol: metadataProtocol, host: candidateArg.host, port, url: candidateArg.host ? `${metadataProtocol}://${candidateArg.host}:${port}` : metadataUrl };
};

const hasMotionEyeHint = (...valuesArgs: Array<string | undefined>): boolean => {
  return valuesArgs.filter(Boolean).join(' ').toLowerCase().includes('motioneye')
    || valuesArgs.filter(Boolean).join(' ').toLowerCase().includes('motion eye');
};

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