import { HunterDouglasPowerViewMapper } from './hunterdouglas_powerview.mapper.js';
import type {
  IHunterDouglasPowerViewCommandRequest,
  IHunterDouglasPowerViewConfig,
  IHunterDouglasPowerViewHubRawData,
  IHunterDouglasPowerViewRawData,
  IHunterDouglasPowerViewRefreshResult,
  IHunterDouglasPowerViewRoomRaw,
  IHunterDouglasPowerViewSceneRaw,
  IHunterDouglasPowerViewShadeRaw,
  IHunterDouglasPowerViewSnapshot,
} from './hunterdouglas_powerview.types.js';
import { hunterDouglasPowerViewDefaultPort, hunterDouglasPowerViewDefaultTimeoutMs } from './hunterdouglas_powerview.types.js';

export class HunterDouglasPowerViewApiError extends Error {}
export class HunterDouglasPowerViewApiConnectionError extends HunterDouglasPowerViewApiError {}
export class HunterDouglasPowerViewUnsupportedCommandError extends HunterDouglasPowerViewApiError {}

interface IHunterDouglasPowerViewEndpoint {
  protocol: 'http' | 'https';
  host: string;
  port?: number;
}

export class HunterDouglasPowerViewClient {
  private currentSnapshot?: IHunterDouglasPowerViewSnapshot;

  constructor(private readonly config: IHunterDouglasPowerViewConfig) {}

  public async getSnapshot(forceRefreshArg = false): Promise<IHunterDouglasPowerViewSnapshot> {
    if (!forceRefreshArg && this.currentSnapshot) {
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (!forceRefreshArg && this.config.snapshot) {
      this.currentSnapshot = HunterDouglasPowerViewMapper.toSnapshot({ config: this.config, source: 'snapshot', online: this.config.snapshot.online });
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (this.config.client) {
      try {
        this.currentSnapshot = await this.snapshotFromClient();
      } catch (errorArg) {
        this.currentSnapshot = HunterDouglasPowerViewMapper.offlineSnapshot(this.config, this.errorMessage(errorArg));
      }
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (!forceRefreshArg && this.hasManualData()) {
      this.currentSnapshot = HunterDouglasPowerViewMapper.toSnapshot({ config: this.config, online: this.config.online ?? true, source: 'manual' });
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (this.hasEndpoint()) {
      try {
        this.currentSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.currentSnapshot = HunterDouglasPowerViewMapper.offlineSnapshot(this.config, this.errorMessage(errorArg));
      }
      return this.cloneSnapshot(this.currentSnapshot);
    }

    this.currentSnapshot = HunterDouglasPowerViewMapper.offlineSnapshot(this.config, 'No PowerView local HTTP endpoint, injected client, snapshot, or manual data is configured.');
    return this.cloneSnapshot(this.currentSnapshot);
  }

  public async refresh(): Promise<IHunterDouglasPowerViewRefreshResult> {
    try {
      this.currentSnapshot = undefined;
      const liveCapable = Boolean(this.config.host || this.config.url || this.config.client);
      const snapshot = await this.getSnapshot(liveCapable);
      const success = liveCapable && snapshot.online && !snapshot.error;
      return { success, snapshot, error: success ? undefined : snapshot.error || 'PowerView refresh requires a live host or injected client.', data: { source: snapshot.source } };
    } catch (errorArg) {
      const error = this.errorMessage(errorArg);
      const snapshot = HunterDouglasPowerViewMapper.offlineSnapshot(this.config, error);
      this.currentSnapshot = snapshot;
      return { success: false, snapshot: this.cloneSnapshot(snapshot), error };
    }
  }

  public async fetchSnapshot(): Promise<IHunterDouglasPowerViewSnapshot> {
    if (!this.hasEndpoint()) {
      throw new HunterDouglasPowerViewApiConnectionError('PowerView local HTTP snapshot requires config.host or config.url.');
    }
    const apiVersion = await this.detectApiVersion();
    const [hub, home, rooms, scenes, shades] = await Promise.all([
      this.fetchHubData(apiVersion),
      apiVersion >= 3 ? this.requestJson<IHunterDouglasPowerViewHubRawData>('home').catch(() => ({})) : Promise.resolve({}),
      this.fetchResources<IHunterDouglasPowerViewRoomRaw>(apiVersion, 'rooms', 'roomData'),
      this.fetchResources<IHunterDouglasPowerViewSceneRaw>(apiVersion, 'scenes', 'sceneData'),
      this.fetchResources<IHunterDouglasPowerViewShadeRaw>(apiVersion, 'shades', 'shadeData'),
    ]);
    const rawData: Partial<IHunterDouglasPowerViewRawData> = {
      apiVersion,
      hub,
      home,
      rooms,
      scenes,
      shades,
      fetchedAt: new Date().toISOString(),
    };
    const snapshot = HunterDouglasPowerViewMapper.toSnapshot({ config: { ...this.config, apiVersion }, rawData, online: true, source: 'http' });
    if (snapshot.hub.role === 'Secondary') {
      throw new HunterDouglasPowerViewApiConnectionError(`${snapshot.hub.name} is the Secondary Hub. Only the Primary can manage shades.`);
    }
    return snapshot;
  }

  public async execute(commandArg: IHunterDouglasPowerViewCommandRequest): Promise<unknown> {
    if (this.config.commandExecutor) {
      return this.config.commandExecutor.execute(commandArg);
    }
    if (this.config.client?.execute) {
      return this.config.client.execute(commandArg);
    }
    if (commandArg.action === 'refresh') {
      return (await this.refresh()).snapshot;
    }
    if (!this.hasEndpoint()) {
      throw new HunterDouglasPowerViewApiConnectionError('PowerView commands require config.host/config.url, an injected client.execute, or commandExecutor. Static snapshots/manual data are read-only.');
    }

    const apiVersion = commandArg.apiVersion || this.config.apiVersion || (await this.getSnapshot()).hub.apiVersion;
    switch (commandArg.action) {
      case 'move_shade': {
        if (!commandArg.shadeId || !commandArg.positions) {
          throw new HunterDouglasPowerViewUnsupportedCommandError('PowerView move_shade requires shadeId and positions.');
        }
        const rawPositions = HunterDouglasPowerViewMapper.rawPositionsForApi(commandArg.positions, apiVersion);
        const result = apiVersion >= 3
          ? await this.requestJson('home/shades/positions', { method: 'PUT', query: { ids: commandArg.shadeId }, body: { positions: rawPositions } })
          : await this.requestJson(`api/shades/${encodeURIComponent(commandArg.shadeId)}`, { method: 'PUT', body: { shade: { id: numericId(commandArg.shadeId), positions: rawPositions } } });
        this.currentSnapshot = undefined;
        return result;
      }
      case 'stop_shade': {
        if (!commandArg.shadeId) {
          throw new HunterDouglasPowerViewUnsupportedCommandError('PowerView stop_shade requires shadeId.');
        }
        const result = apiVersion >= 3
          ? await this.requestJson('home/shades/stop', { method: 'PUT', query: { ids: commandArg.shadeId } })
          : await this.requestJson(`api/shades/${encodeURIComponent(commandArg.shadeId)}`, { method: 'PUT', body: { shade: { motion: 'stop' } } });
        this.currentSnapshot = undefined;
        return result;
      }
      case 'activate_scene': {
        if (!commandArg.sceneId) {
          throw new HunterDouglasPowerViewUnsupportedCommandError('PowerView activate_scene requires sceneId.');
        }
        const result = apiVersion >= 3
          ? await this.requestJson(`home/scenes/${encodeURIComponent(commandArg.sceneId)}/activate`, { method: 'PUT' })
          : await this.requestJson('api/scenes', { query: { sceneId: commandArg.sceneId } });
        this.currentSnapshot = undefined;
        return result;
      }
      case 'raw_request': {
        if (!commandArg.path) {
          throw new HunterDouglasPowerViewUnsupportedCommandError('PowerView raw_request requires path.');
        }
        const result = await this.requestJson(commandArg.path, { method: commandArg.method || 'GET', query: commandArg.query, body: commandArg.body });
        this.currentSnapshot = undefined;
        return result;
      }
      default:
        throw new HunterDouglasPowerViewUnsupportedCommandError(`Unsupported PowerView command: ${commandArg.action}`);
    }
  }

  public async destroy(): Promise<void> {
    await this.config.client?.destroy?.();
  }

  private async detectApiVersion(): Promise<number> {
    if (this.config.apiVersion) {
      return this.config.apiVersion;
    }
    const legacy = await this.requestJson<IHunterDouglasPowerViewHubRawData>('api/fwversion').catch(() => undefined);
    const legacyRevision = this.apiVersionFromHubData(legacy);
    if (legacyRevision) {
      return legacyRevision;
    }
    const generation3 = await this.requestJson<IHunterDouglasPowerViewHubRawData>('gateway/info').catch(() => undefined);
    const generation3Revision = this.apiVersionFromHubData(generation3);
    if (generation3Revision) {
      return generation3Revision;
    }
    const gateway = await this.requestJson<IHunterDouglasPowerViewHubRawData>('gateway').catch(() => undefined);
    const gatewayRevision = this.apiVersionFromHubData(gateway);
    if (gatewayRevision) {
      return gatewayRevision;
    }
    return 2;
  }

  private async fetchHubData(apiVersionArg: number): Promise<IHunterDouglasPowerViewHubRawData> {
    if (apiVersionArg >= 3) {
      return this.requestJson<IHunterDouglasPowerViewHubRawData>('gateway');
    }
    return this.requestJson<IHunterDouglasPowerViewHubRawData>('api/userdata');
  }

  private async fetchResources<TValue>(apiVersionArg: number, resourceArg: 'rooms' | 'scenes' | 'shades', legacyKeyArg: 'roomData' | 'sceneData' | 'shadeData'): Promise<TValue[]> {
    const path = `${apiVersionArg >= 3 ? 'home' : 'api'}/${resourceArg}`;
    const data = await this.requestJson<Record<string, unknown> | TValue[]>(path);
    if (Array.isArray(data)) {
      return data as TValue[];
    }
    const legacy = data[legacyKeyArg];
    return Array.isArray(legacy) ? legacy as TValue[] : [];
  }

  private async snapshotFromClient(): Promise<IHunterDouglasPowerViewSnapshot> {
    const client = this.config.client;
    if (!client) {
      throw new HunterDouglasPowerViewApiConnectionError('No PowerView client configured.');
    }
    if (client.getSnapshot) {
      const result = await client.getSnapshot();
      if (this.isSnapshot(result)) {
        return HunterDouglasPowerViewMapper.toSnapshot({ config: { ...this.config, snapshot: result }, source: 'client', online: result.online });
      }
      return HunterDouglasPowerViewMapper.toSnapshot({ config: this.config, rawData: result, online: true, source: 'client' });
    }
    if (client.getRawData) {
      return HunterDouglasPowerViewMapper.toSnapshot({ config: this.config, rawData: await client.getRawData(), online: true, source: 'client' });
    }
    throw new HunterDouglasPowerViewApiConnectionError('PowerView client must expose getSnapshot() or getRawData().');
  }

  private async requestJson<TValue = unknown>(pathArg: string, optionsArg: { method?: 'GET' | 'PUT' | 'POST' | 'DELETE'; body?: unknown; query?: Record<string, string | number | boolean | undefined> } = {}): Promise<TValue> {
    const url = this.url(pathArg, optionsArg.query);
    let response: Response;
    try {
      response = await globalThis.fetch(url, {
        method: optionsArg.method || 'GET',
        headers: optionsArg.body === undefined ? undefined : { 'content-type': 'application/json' },
        body: optionsArg.body === undefined ? undefined : JSON.stringify(optionsArg.body),
        signal: AbortSignal.timeout(this.config.timeoutMs || hunterDouglasPowerViewDefaultTimeoutMs),
      });
    } catch (errorArg) {
      throw new HunterDouglasPowerViewApiConnectionError(`Connection to ${url} failed: ${this.errorMessage(errorArg)}`);
    }

    if (response.status === 423) {
      await response.arrayBuffer().catch(() => undefined);
      throw new HunterDouglasPowerViewApiConnectionError('PowerView Hub is undergoing maintenance.');
    }
    if (!response.ok) {
      const text = await response.text().catch(() => '');
      throw new HunterDouglasPowerViewApiConnectionError(`PowerView endpoint ${url} failed with HTTP ${response.status}${text ? `: ${text}` : ''}`);
    }
    if (response.status === 204) {
      await response.arrayBuffer().catch(() => undefined);
      return {} as TValue;
    }
    const text = await response.text();
    if (!text.trim()) {
      return {} as TValue;
    }
    try {
      return JSON.parse(text) as TValue;
    } catch (errorArg) {
      throw new HunterDouglasPowerViewApiConnectionError(`PowerView endpoint ${url} did not return JSON: ${this.errorMessage(errorArg)}`);
    }
  }

  private url(pathArg: string, queryArg?: Record<string, string | number | boolean | undefined>): string {
    if (/^https?:\/\//i.test(pathArg)) {
      const absolute = new URL(pathArg);
      this.applyQuery(absolute, queryArg);
      return absolute.toString();
    }
    const url = new URL(pathArg.startsWith('/') ? pathArg : `/${pathArg}`, `${this.baseUrl()}/`);
    this.applyQuery(url, queryArg);
    return url.toString();
  }

  private applyQuery(urlArg: URL, queryArg?: Record<string, string | number | boolean | undefined>): void {
    for (const [key, value] of Object.entries(queryArg || {})) {
      if (value !== undefined) {
        urlArg.searchParams.set(key, String(value));
      }
    }
  }

  private baseUrl(): string {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new HunterDouglasPowerViewApiConnectionError('PowerView request requires config.host or config.url.');
    }
    const port = endpoint.port && !((endpoint.protocol === 'http' && endpoint.port === 80) || (endpoint.protocol === 'https' && endpoint.port === 443)) ? `:${endpoint.port}` : '';
    return `${endpoint.protocol}://${this.hostForUrl(endpoint.host)}${port}`;
  }

  private endpoint(): IHunterDouglasPowerViewEndpoint | undefined {
    const configured = this.config.url || this.config.host;
    if (!configured) {
      return undefined;
    }
    try {
      const url = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(configured) ? configured : `http://${configured}`);
      return {
        protocol: url.protocol === 'https:' ? 'https' : 'http',
        host: url.hostname,
        port: this.config.port || (url.port ? Number(url.port) : undefined),
      };
    } catch {
      return { protocol: 'http', host: configured, port: this.config.port || hunterDouglasPowerViewDefaultPort };
    }
  }

  private apiVersionFromHubData(valueArg: IHunterDouglasPowerViewHubRawData | undefined): number | undefined {
    if (!valueArg) {
      return undefined;
    }
    const revision = numberValue(valueAt(valueArg, ['userData', 'firmware', 'mainProcessor', 'revision']))
      || numberValue(valueAt(valueArg, ['config', 'firmware', 'mainProcessor', 'revision']))
      || numberValue(valueAt(valueArg, ['firmware', 'mainProcessor', 'revision']));
    if (revision) {
      return revision;
    }
    const fwVersion = stringValue(valueArg.fwVersion);
    if (fwVersion) {
      return Number(fwVersion.split('.')[0]) || undefined;
    }
    return undefined;
  }

  private hasEndpoint(): boolean {
    return Boolean(this.config.host || this.config.url);
  }

  private hasManualData(): boolean {
    return Boolean(this.config.rawData || this.config.snapshot);
  }

  private isSnapshot(valueArg: unknown): valueArg is IHunterDouglasPowerViewSnapshot {
    return Boolean(valueArg && typeof valueArg === 'object' && 'hub' in valueArg && 'shades' in valueArg && 'scenes' in valueArg);
  }

  private hostForUrl(hostArg: string): string {
    return hostArg.includes(':') && !hostArg.startsWith('[') ? `[${hostArg}]` : hostArg;
  }

  private errorMessage(errorArg: unknown): string {
    return errorArg instanceof Error ? errorArg.message : String(errorArg);
  }

  private cloneSnapshot(snapshotArg: IHunterDouglasPowerViewSnapshot): IHunterDouglasPowerViewSnapshot {
    return JSON.parse(JSON.stringify(snapshotArg)) as IHunterDouglasPowerViewSnapshot;
  }
}

const numericId = (valueArg: string): string | number => /^\d+$/.test(valueArg) ? Number(valueArg) : valueArg;
const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
const numberValue = (valueArg: unknown): number | undefined => {
  if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
    return valueArg;
  }
  if (typeof valueArg === 'string' && valueArg.trim()) {
    const parsed = Number(valueArg);
    return Number.isFinite(parsed) ? parsed : undefined;
  }
  return undefined;
};
const valueAt = (valueArg: Record<string, unknown> | undefined, pathArg: string[]): unknown => {
  let current: unknown = valueArg;
  for (const key of pathArg) {
    if (!current || typeof current !== 'object' || Array.isArray(current)) {
      return undefined;
    }
    current = (current as Record<string, unknown>)[key];
  }
  return current;
};
