import type { IConfigFlow, IConfigFlowContext, IConfigFlowStep, IDiscoveryCandidate } from '../../core/types.js';
import { BondClient } from './bond.classes.client.js';
import type { IBondConfig, IBondRawData, IBondSnapshot } from './bond.types.js';
import { bondDefaultPort, bondDefaultTimeoutMs } from './bond.types.js';

export class BondConfigFlow implements IConfigFlow<IBondConfig> {
  public async start(candidateArg: IDiscoveryCandidate, contextArg: IConfigFlowContext): Promise<IConfigFlowStep<IBondConfig>> {
    void contextArg;
    return {
      kind: 'form',
      title: 'Configure Bond',
      description: 'Configure a local Bond Bridge endpoint. Discovery candidates need an access token unless a live token is available from the bridge setup window or snapshot/client data is supplied.',
      fields: [
        { name: 'host', label: 'Host', type: 'text' },
        { name: 'port', label: 'Port', type: 'number' },
        { name: 'accessToken', label: 'Access token', type: 'password' },
        { name: 'name', label: 'Name', type: 'text' },
      ],
      submit: async (valuesArg) => this.submit(candidateArg, valuesArg),
    };
  }

  private async submit(candidateArg: IDiscoveryCandidate, valuesArg: Record<string, unknown>): Promise<IConfigFlowStep<IBondConfig>> {
    const metadata = candidateArg.metadata || {};
    const parsed = parseEndpoint(this.stringValue(valuesArg.host) || this.stringMetadata(metadata, 'url'));
    const snapshot = snapshotValue(metadata.snapshot);
    const rawData = rawDataValue(metadata.rawData);
    const host = parsed?.host || this.stringValue(valuesArg.host) || candidateArg.host || snapshot?.hub.host;
    const port = this.numberValue(valuesArg.port) || parsed?.port || candidateArg.port || snapshot?.hub.port || bondDefaultPort;
    let accessToken = this.stringValue(valuesArg.accessToken)
      || this.stringValue(valuesArg.token)
      || this.stringMetadata(metadata, 'accessToken')
      || this.stringMetadata(metadata, 'token');
    const hasManualData = Boolean(snapshot || rawData || metadata.client);
    const discoveryProtocol = this.stringMetadata(metadata, 'discoveryProtocol') || candidateArg.source;

    if (host && !accessToken && !hasManualData && ['zeroconf', 'dhcp', 'mdns'].includes(discoveryProtocol)) {
      accessToken = await BondClient.fetchToken(host, port, 1500).catch(() => undefined);
    }

    if (!host && !hasManualData) {
      return { kind: 'error', title: 'Bond setup failed', error: 'Bond host, injected client, snapshot, or manual raw data is required.' };
    }
    if (!this.validPort(port)) {
      return { kind: 'error', title: 'Bond setup failed', error: 'Bond port must be between 1 and 65535.' };
    }
    if (host && !accessToken && !hasManualData) {
      return { kind: 'error', title: 'Bond setup failed', error: 'Bond access token is required for local bridge HTTP access.' };
    }

    const config: IBondConfig = {
      host,
      port,
      accessToken,
      timeoutMs: bondDefaultTimeoutMs,
      name: this.stringValue(valuesArg.name) || candidateArg.name || snapshot?.hub.name || this.stringMetadata(metadata, 'name'),
      manufacturer: candidateArg.manufacturer || snapshot?.hub.manufacturer || 'Olibra',
      model: candidateArg.model || snapshot?.hub.model || snapshot?.hub.target,
      uniqueId: candidateArg.id || snapshot?.hub.id || (host ? `${host}:${port}` : undefined),
      bondId: candidateArg.id || snapshot?.hub.id,
      snapshot,
      rawData,
      client: metadata.client as IBondConfig['client'],
      commandExecutor: metadata.commandExecutor as IBondConfig['commandExecutor'],
    };

    return { kind: 'done', title: 'Bond configured', config };
  }

  private stringValue(valueArg: unknown): string | undefined {
    return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim().replace(/\/$/, '') : undefined;
  }

  private numberValue(valueArg: unknown): number | undefined {
    if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
      return Math.round(valueArg);
    }
    if (typeof valueArg === 'string' && valueArg.trim()) {
      const parsed = Number(valueArg);
      return Number.isFinite(parsed) ? Math.round(parsed) : undefined;
    }
    return undefined;
  }

  private stringMetadata(metadataArg: Record<string, unknown>, keyArg: string): string | undefined {
    return this.stringValue(metadataArg[keyArg]);
  }

  private validPort(valueArg: number): boolean {
    return Number.isInteger(valueArg) && valueArg > 0 && valueArg <= 65535;
  }
}

const parseEndpoint = (valueArg: string | undefined): { host: string; port?: number } | undefined => {
  if (!valueArg) {
    return undefined;
  }
  try {
    const url = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(valueArg) ? valueArg : `http://${valueArg}`);
    return { host: url.hostname, port: url.port ? Number(url.port) : undefined };
  } catch {
    return undefined;
  }
};

const snapshotValue = (valueArg: unknown): IBondSnapshot | undefined => valueArg && typeof valueArg === 'object' && 'hub' in valueArg && 'devices' in valueArg ? valueArg as IBondSnapshot : undefined;

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