import { SimpleLocalClient, type IServiceCallRequest, type IServiceCallResult, type ISimpleLocalSnapshot } from '../../core/index.js';
import { MfiMapper } from './mfi.mapper.js';
import type { IMfiConfig, IMfiNativeRawData, IMfiPortSnapshot } from './mfi.types.js';
import { mfiDefaultHttpPort, mfiDefaultHttpsPort, mfiDefaultTimeoutMs, mfiProfile } from './mfi.types.js';

export class MfiApiError extends Error {}
export class MfiApiConnectionError extends MfiApiError {}
export class MfiApiAuthenticationError extends MfiApiError {}
export class MfiApiUnsupportedError extends MfiApiError {}

export class MfiClient extends SimpleLocalClient<IMfiConfig> {
  private liveSnapshot?: ISimpleLocalSnapshot;
  private readonly mfiConfig: IMfiConfig;
  private readonly cookies = new Map<string, string>();
  private loggedIn = false;

  constructor(configArg: IMfiConfig) {
    super(mfiProfile, configArg);
    this.mfiConfig = configArg;
  }

  public async getSnapshot(forceRefreshArg = false): Promise<ISimpleLocalSnapshot> {
    if (!forceRefreshArg && this.liveSnapshot) {
      return clone(this.liveSnapshot);
    }

    if (!this.canUseLiveHttp()) {
      return super.getSnapshot(forceRefreshArg);
    }

    try {
      const rawData = await this.getRawData();
      this.liveSnapshot = MfiMapper.toSnapshot({ config: this.mfiConfig, rawData, online: true, source: 'http' });
    } catch (errorArg) {
      this.liveSnapshot = MfiMapper.toSnapshot({ config: this.mfiConfig, online: false, source: 'runtime', error: errorMessage(errorArg) });
    }
    return clone(this.liveSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    if (!this.canUseLiveHttp()) {
      return super.refresh();
    }
    this.liveSnapshot = undefined;
    const snapshot = await this.getSnapshot(true);
    return { success: snapshot.online && !snapshot.error, error: snapshot.error, data: snapshot };
  }

  public async getRawData(): Promise<IMfiNativeRawData> {
    if (!this.canUseLiveHttp()) {
      throw new MfiApiConnectionError('mFi local HTTP polling requires config.host or config.url.');
    }
    await this.login();
    const [devices, sensors] = await Promise.all([
      this.requestJsonData<Array<Record<string, unknown>>>('/api/v1.0/stat/device', { method: 'GET' }),
      this.requestJsonData<Array<Record<string, unknown>>>('/api/v1.0/list/sensors', { method: 'POST', form: { json: JSON.stringify({ hello: 2 }) } }),
    ]);
    return { devices, sensors, fetchedAt: new Date().toISOString() };
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.mfiConfig.commandExecutor) {
      return normalizeServiceResult(await this.mfiConfig.commandExecutor.execute(requestArg));
    }
    if (!this.canUseLiveHttp()) {
      return super.execute(requestArg);
    }
    if (requestArg.service === 'refresh') {
      return this.refresh();
    }

    const desiredState = await this.desiredSwitchState(requestArg);
    if (desiredState === undefined) {
      return { success: false, error: `Unsupported mFi service: ${requestArg.domain}.${requestArg.service}` };
    }

    try {
      const rawData = await this.getRawData();
      const port = this.portForRequest(rawData, requestArg);
      if (!port) {
        return { success: false, error: 'No matching mFi switch port found for the service target.' };
      }
      await this.controlPort(port, desiredState, rawData);
      this.liveSnapshot = undefined;
      return { success: true, data: { portId: port.id, label: port.label, state: desiredState } };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

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

  private async desiredSwitchState(requestArg: IServiceCallRequest): Promise<boolean | undefined> {
    if (requestArg.service === 'turn_on') {
      return true;
    }
    if (requestArg.service === 'turn_off') {
      return false;
    }
    if (requestArg.service === 'toggle') {
      const snapshot = await this.getSnapshot();
      const entity = this.entityForTarget(snapshot, requestArg);
      return entity?.state === undefined ? undefined : !Boolean(entity.state);
    }
    return undefined;
  }

  private portForRequest(rawDataArg: IMfiNativeRawData, requestArg: IServiceCallRequest): IMfiPortSnapshot | undefined {
    const ports = MfiMapper.portsFromRawData(rawDataArg).filter((portArg) => mfiSwitchModels.has(portArg.model));
    const requestedPort = stringValue(requestArg.data?.portId ?? requestArg.data?.ident ?? requestArg.data?.id);
    if (requestedPort) {
      return ports.find((portArg) => portArg.id === requestedPort || portArg.port === requestedPort);
    }
    const requestedLabel = stringValue(requestArg.data?.label ?? requestArg.data?.device);
    if (requestedLabel) {
      const label = requestedLabel.toLowerCase();
      return ports.find((portArg) => portArg.label.toLowerCase() === label);
    }
    const target = targetText(requestArg);
    if (target) {
      return ports.find((portArg) => target.includes(MfiMapper.slug(portArg.id)) || target.includes(MfiMapper.slug(portArg.label)) || target.includes(MfiMapper.slug(`switch_${portArg.id}`)));
    }
    return ports.length === 1 ? ports[0] : undefined;
  }

  private entityForTarget(snapshotArg: ISimpleLocalSnapshot, requestArg: IServiceCallRequest): ISimpleLocalSnapshot['entities'][number] | undefined {
    const switches = snapshotArg.entities.filter((entityArg) => entityArg.platform === 'switch');
    const target = targetText(requestArg);
    if (target) {
      return switches.find((entityArg) => target.includes(MfiMapper.slug(entityArg.id)) || target.includes(MfiMapper.slug(entityArg.name)) || (entityArg.uniqueId && target.includes(MfiMapper.slug(entityArg.uniqueId))));
    }
    return switches.length === 1 ? switches[0] : undefined;
  }

  private async controlPort(portArg: IMfiPortSnapshot, stateArg: boolean, rawDataArg: IMfiNativeRawData): Promise<string> {
    const portConfig = this.findPortConfig(rawDataArg, portArg.id);
    if (!portConfig) {
      throw new MfiApiUnsupportedError(`mFi port ${portArg.id} was not present in stat/device port_cfg.`);
    }

    const model = stringValue(portConfig.model) || portArg.model;
    const voltage = stateArg ? mfiOutputVoltages[model] || 0 : 0;
    const payload = {
      sId: portArg.id,
      mac: stringValue(portConfig.mac) || portArg.mac,
      model,
      port: numberValue(portConfig.port) ?? numberValue(portArg.port) ?? 0,
      cmd: 'mfi-output',
      val: stateArg ? 1 : 0,
      volt: voltage,
    };
    return this.requestText('/api/v1.0/cmd/devmgr', { method: 'POST', form: { json: JSON.stringify(payload) }, retryLogin: true });
  }

  private findPortConfig(rawDataArg: IMfiNativeRawData, portIdArg: string): Record<string, unknown> | undefined {
    for (const device of rawDataArg.devices || []) {
      const portConfigs = Array.isArray(device.port_cfg) ? device.port_cfg as Array<Record<string, unknown>> : [];
      const port = portConfigs.find((portArg) => stringValue(portArg._id) === portIdArg);
      if (port) {
        return port;
      }
    }
    return undefined;
  }

  private async login(): Promise<void> {
    if (this.loggedIn) {
      return;
    }
    this.cookies.clear();
    await this.requestText('/', { method: 'GET', retryLogin: false }).catch(() => undefined);
    const response = await this.request('/login', {
      method: 'POST',
      form: { username: this.mfiConfig.username || '', password: this.mfiConfig.password || '', login: 'Login' },
      redirect: 'manual',
      retryLogin: false,
    });
    const location = response.headers.get('location') || response.url;
    await response.arrayBuffer().catch(() => undefined);
    if (response.status >= 300 && response.status < 400 && location.endsWith('/manage')) {
      this.loggedIn = true;
      return;
    }
    if (response.ok && response.url.endsWith('/manage')) {
      this.loggedIn = true;
      return;
    }
    throw new MfiApiAuthenticationError('mFi Controller rejected login credentials.');
  }

  private async requestJsonData<TValue>(pathArg: string, optionsArg: IMfiRequestOptions): Promise<TValue> {
    const data = await this.requestJson<Record<string, unknown>>(pathArg, { ...optionsArg, retryLogin: true });
    if (!data || typeof data !== 'object' || !Array.isArray(data.data)) {
      throw new MfiApiConnectionError(`mFi endpoint ${pathArg} did not return a data array.`);
    }
    return data.data as TValue;
  }

  private async requestJson<TValue>(pathArg: string, optionsArg: IMfiRequestOptions): Promise<TValue> {
    const text = await this.requestText(pathArg, optionsArg);
    try {
      return JSON.parse(text) as TValue;
    } catch (errorArg) {
      throw new MfiApiConnectionError(`Unable to parse mFi JSON from ${pathArg}: ${errorMessage(errorArg)}`);
    }
  }

  private async requestText(pathArg: string, optionsArg: IMfiRequestOptions): Promise<string> {
    const response = await this.request(pathArg, optionsArg);
    const text = await response.text();
    if (!response.ok) {
      if (optionsArg.retryLogin !== false) {
        this.loggedIn = false;
        await this.login();
        return this.requestText(pathArg, { ...optionsArg, retryLogin: false });
      }
      throw new MfiApiConnectionError(`mFi endpoint ${pathArg} failed with HTTP ${response.status}${text ? `: ${text}` : ''}`);
    }
    return text;
  }

  private async request(pathArg: string, optionsArg: IMfiRequestOptions): Promise<Response> {
    const endpoint = this.endpoint();
    if (endpoint.protocol === 'https' && this.mfiConfig.verifySsl === false) {
      throw new MfiApiUnsupportedError('mFi verifySsl=false is unsupported with global fetch; use HTTP, a trusted certificate, or an injected client.');
    }
    const url = new URL(pathArg.startsWith('/') ? pathArg : `/${pathArg}`, `${endpoint.protocol}://${hostForUrl(endpoint.host)}:${endpoint.port}`);
    const headers = new Headers(optionsArg.headers);
    const cookie = this.cookieHeader();
    if (cookie) {
      headers.set('cookie', cookie);
    }
    let body: BodyInit | undefined;
    if (optionsArg.form) {
      body = new URLSearchParams(optionsArg.form);
    } else if (optionsArg.body !== undefined) {
      body = optionsArg.body;
    }

    let response: Response;
    try {
      response = await globalThis.fetch(url, {
        method: optionsArg.method || 'GET',
        headers,
        body,
        redirect: optionsArg.redirect || 'follow',
        signal: AbortSignal.timeout(this.mfiConfig.timeoutMs || mfiDefaultTimeoutMs),
      });
    } catch (errorArg) {
      throw new MfiApiConnectionError(`Connection to ${url.toString()} failed: ${errorMessage(errorArg)}`);
    }
    this.storeCookies(response);
    return response;
  }

  private endpoint(): { protocol: 'http' | 'https'; host: string; port: number } {
    const configured = this.mfiConfig.url || this.mfiConfig.host;
    if (!configured) {
      throw new MfiApiConnectionError('mFi local HTTP requests require config.host or config.url.');
    }
    const useTls = this.mfiConfig.useTls ?? this.mfiConfig.ssl ?? true;
    if (/^https?:\/\//i.test(configured)) {
      const url = new URL(configured);
      const protocol = url.protocol === 'http:' ? 'http' : 'https';
      return {
        protocol,
        host: url.hostname,
        port: this.mfiConfig.port || (url.port ? Number(url.port) : undefined) || (protocol === 'https' ? mfiDefaultHttpsPort : mfiDefaultHttpPort),
      };
    }
    return {
      protocol: useTls ? 'https' : 'http',
      host: configured,
      port: this.mfiConfig.port || (useTls ? mfiDefaultHttpsPort : mfiDefaultHttpPort),
    };
  }

  private canUseLiveHttp(): boolean {
    return Boolean(this.mfiConfig.host || this.mfiConfig.url);
  }

  private storeCookies(responseArg: Response): void {
    const headers = responseArg.headers as Headers & { getSetCookie?: () => string[] };
    const values = headers.getSetCookie?.() || splitSetCookieHeader(headers.get('set-cookie'));
    for (const value of values) {
      const cookie = value.split(';', 1)[0];
      const separatorIndex = cookie.indexOf('=');
      if (separatorIndex <= 0) {
        continue;
      }
      this.cookies.set(cookie.slice(0, separatorIndex), cookie.slice(separatorIndex + 1));
    }
  }

  private cookieHeader(): string | undefined {
    const values = [...this.cookies.entries()].map(([key, value]) => `${key}=${value}`);
    return values.length ? values.join('; ') : undefined;
  }
}

interface IMfiRequestOptions {
  method?: 'GET' | 'POST';
  headers?: HeadersInit;
  body?: BodyInit;
  form?: Record<string, string>;
  redirect?: RequestRedirect;
  retryLogin?: boolean;
}

const mfiSwitchModels = new Set(['Outlet', 'Output 5v', 'Output 12v', 'Output 24v', 'Dimmer Switch']);
const mfiOutputVoltages: Record<string, number> = { 'Output 5v': 5, 'Output 12v': 12, 'Output 24v': 24 };

const normalizeServiceResult = (valueArg: unknown): IServiceCallResult => {
  if (valueArg && typeof valueArg === 'object' && 'success' in valueArg && typeof (valueArg as IServiceCallResult).success === 'boolean') {
    return valueArg as IServiceCallResult;
  }
  return { success: true, data: valueArg };
};

const splitSetCookieHeader = (valueArg: string | null): string[] => {
  if (!valueArg) {
    return [];
  }
  return valueArg.split(/,(?=\s*[^;,\s]+=)/g).map((value) => value.trim()).filter(Boolean);
};

const targetText = (requestArg: IServiceCallRequest): string => {
  return [requestArg.target.entityId, requestArg.target.deviceId].filter((valueArg): valueArg is string => Boolean(valueArg)).map((valueArg) => MfiMapper.slug(valueArg)).join(' ');
};

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

const stringValue = (valueArg: unknown): string | undefined => {
  if (typeof valueArg === 'string' && valueArg.trim()) {
    return valueArg.trim();
  }
  if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
    return String(valueArg);
  }
  return 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 errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;
