import { SimpleLocalClient, type ISimpleLocalEntitySnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { OverseerrMapper } from './overseerr.mapper.js';
import type { IOverseerrConfig, IOverseerrSnapshot } from './overseerr.types.js';
import { overseerrDefaultName, overseerrProfile } from './overseerr.types.js';

interface IOverseerrEndpoint {
  apiBase: string;
  origin: string;
  host: string;
  port?: number;
  protocol: 'http' | 'https';
}

interface IOverseerrRawData {
  requests?: Record<string, unknown>;
  issues?: Record<string, unknown>;
  status?: Record<string, unknown>;
}

interface IOverseerrRequestOptions {
  method?: 'GET' | 'POST';
  params?: Record<string, string | number | boolean | undefined>;
  body?: unknown;
}

export class OverseerrClient extends SimpleLocalClient<IOverseerrConfig> {
  private currentOverseerrSnapshot?: IOverseerrSnapshot;

  constructor(private readonly configArg: IOverseerrConfig) {
    super(overseerrProfile, configArg);
  }

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

    if (this.shouldUseLiveApi()) {
      try {
        this.currentOverseerrSnapshot = await this.fetchLiveSnapshot();
      } catch (errorArg) {
        this.currentOverseerrSnapshot = this.offlineOverseerrSnapshot(errorMessage(errorArg));
      }
      return clone(this.currentOverseerrSnapshot);
    }

    if (this.endpoint() && !this.apiKey() && !this.hasOfflineSource()) {
      this.currentOverseerrSnapshot = this.offlineOverseerrSnapshot('Seerr live local API polling requires config.apiKey or config.token for X-Api-Key authentication.');
      return clone(this.currentOverseerrSnapshot);
    }

    return await super.getSnapshot(forceRefreshArg) as IOverseerrSnapshot;
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (requestArg.service !== 'get_requests') {
      return super.execute(requestArg);
    }

    if (this.shouldUseLiveApi()) {
      try {
        return { success: true, data: { requests: await this.getRequests(requestArg.data || {}) } };
      } catch (errorArg) {
        return { success: false, error: errorMessage(errorArg) };
      }
    }

    if (this.configArg.client?.execute || this.configArg.commandExecutor) {
      return super.execute(requestArg);
    }

    return {
      success: false,
      error: 'Seerr service get_requests requires config.host/config.url plus config.apiKey/config.token, an injected client.execute, or commandExecutor.',
    };
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.currentOverseerrSnapshot = undefined;
    const liveCapable = Boolean(this.hasOfflineSource() || this.shouldUseLiveApi());
    const snapshot = await this.getSnapshot(liveCapable);
    const success = liveCapable && snapshot.online && !snapshot.error && snapshot.source !== 'runtime';
    return { success, error: success ? undefined : snapshot.error || 'Seerr refresh requires a live HTTP endpoint, injected client, or snapshotProvider.', data: snapshot };
  }

  public async fetchLiveSnapshot(): Promise<IOverseerrSnapshot> {
    const [requests, issues, status] = await Promise.all([
      this.requestJson<Record<string, unknown>>('request/count'),
      this.requestJson<Record<string, unknown>>('issue/count'),
      this.optionalJson<Record<string, unknown>>('status'),
    ]);
    const rawData: IOverseerrRawData = { requests, issues, status };
    const endpoint = this.requireEndpoint();
    return OverseerrMapper.toSnapshot({
      config: this.configArg,
      rawData: {
        device: {
          id: this.configArg.uniqueId || endpoint.host || overseerrProfile.domain,
          name: this.configArg.name || overseerrDefaultName,
          manufacturer: 'Seerr',
          model: status?.version ? `Seerr ${status.version}` : 'Seerr server',
          host: endpoint.host,
          port: endpoint.port,
          protocol: endpoint.protocol,
          configurationUrl: endpoint.origin,
          attributes: cleanAttributes({ version: status?.version, updateAvailable: status?.updateAvailable }),
        },
        entities: this.entitiesFromCounts(rawData),
        online: true,
        updatedAt: new Date().toISOString(),
        source: 'http',
        rawData,
        capabilities: {
          localRead: true,
          localControl: true,
          services: ['get_requests'],
        },
      },
      online: true,
      source: 'http',
    }) as IOverseerrSnapshot;
  }

  public async getRequests(filtersArg: Record<string, unknown> = {}): Promise<unknown[]> {
    const params: Record<string, string | number | boolean | undefined> = {
      filter: stringValue(filtersArg.status) || stringValue(filtersArg.filter),
      sort: stringValue(filtersArg.sort_order) || stringValue(filtersArg.sortOrder) || stringValue(filtersArg.sort),
      requestedBy: numberValue(filtersArg.requested_by) ?? numberValue(filtersArg.requestedBy),
    };
    const response = await this.requestJson<{ results?: unknown[] } | unknown[]>('request', { params });
    const requests = Array.isArray(response) ? response : Array.isArray(response.results) ? response.results : [];
    return Promise.all(requests.map((requestArg) => this.withMediaDetails(requestArg)));
  }

  private entitiesFromCounts(rawDataArg: IOverseerrRawData): ISimpleLocalEntitySnapshot[] {
    const requests = rawDataArg.requests || {};
    const issues = rawDataArg.issues || {};
    const entities: ISimpleLocalEntitySnapshot[] = [
      this.sensor('total_requests', 'Total requests', requests.total, 'requests', 'total'),
      this.sensor('movie_requests', 'Movie requests', requests.movie, 'requests', 'total'),
      this.sensor('tv_requests', 'TV requests', requests.tv, 'requests', 'total'),
      this.sensor('pending_requests', 'Pending requests', requests.pending, 'requests', 'total'),
      this.sensor('declined_requests', 'Declined requests', requests.declined, 'requests', 'total'),
      this.sensor('processing_requests', 'Processing requests', requests.processing, 'requests', 'total'),
      this.sensor('available_requests', 'Available requests', requests.available, 'requests', 'total'),
      this.sensor('total_issues', 'Total issues', issues.total, 'issues', 'total'),
      this.sensor('open_issues', 'Open issues', issues.open, 'issues', 'total'),
      this.sensor('closed_issues', 'Closed issues', issues.closed, 'issues', 'total'),
      this.sensor('video_issues', 'Video issues', issues.video, 'issues', 'total'),
      this.sensor('audio_issues', 'Audio issues', issues.audio, 'issues', 'total'),
      this.sensor('subtitle_issues', 'Subtitle issues', issues.subtitles, 'issues', 'total'),
    ];

    if (rawDataArg.status?.version) {
      entities.push(this.sensor('version', 'Version', rawDataArg.status.version));
    }
    if (rawDataArg.status && typeof rawDataArg.status.updateAvailable === 'boolean') {
      entities.push({ id: 'update_available', name: 'Update available', platform: 'binary_sensor', state: rawDataArg.status.updateAvailable, available: true });
    }

    return entities.filter((entityArg) => entityArg.state !== undefined);
  }

  private sensor(idArg: string, nameArg: string, stateArg: unknown, unitArg?: string, stateClassArg?: string): ISimpleLocalEntitySnapshot {
    return {
      id: idArg,
      name: nameArg,
      platform: 'sensor',
      state: stateArg,
      available: stateArg !== undefined,
      unit: unitArg,
      stateClass: stateClassArg,
    };
  }

  private async withMediaDetails(requestArg: unknown): Promise<unknown> {
    const request = recordValue(requestArg);
    const media = recordValue(request?.media);
    const mediaType = stringValue(media?.mediaType) || stringValue(media?.media_type);
    const tmdbId = numberValue(media?.tmdbId) ?? numberValue(media?.tmdb_id);
    if (!request || !mediaType || !tmdbId || !['movie', 'tv'].includes(mediaType)) {
      return requestArg;
    }
    try {
      return { ...request, mediaDetails: await this.requestJson(`${mediaType}/${tmdbId}`) };
    } catch (errorArg) {
      return { ...request, mediaDetails: {}, mediaDetailsError: errorMessage(errorArg) };
    }
  }

  private async optionalJson<TValue>(pathArg: string): Promise<TValue | undefined> {
    try {
      return await this.requestJson<TValue>(pathArg);
    } catch {
      return undefined;
    }
  }

  private async requestJson<TValue = unknown>(pathArg: string, optionsArg: IOverseerrRequestOptions = {}): Promise<TValue> {
    const endpoint = this.requireEndpoint();
    const apiKey = this.apiKey();
    if (!apiKey) {
      throw new Error('Seerr live local API requests require config.apiKey or config.token.');
    }
    const url = new URL(`${endpoint.apiBase.replace(/\/+$/, '')}/${pathArg.replace(/^\/+/, '')}`);
    for (const [key, value] of Object.entries(optionsArg.params || {})) {
      if (value !== undefined) {
        url.searchParams.set(key, String(value));
      }
    }

    const headers: Record<string, string> = {
      accept: 'application/json',
      'x-api-key': apiKey,
      'user-agent': 'SmarthomeExchange-Seerr/1.0',
    };
    let body: string | undefined;
    if (optionsArg.body !== undefined) {
      headers['content-type'] = 'application/json';
      body = JSON.stringify(optionsArg.body);
    }

    const timeoutMs = this.timeoutMs();
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      const response = await globalThis.fetch(url, {
        method: optionsArg.method || 'GET',
        headers,
        body,
        signal: controller.signal,
      });
      const text = await response.text();
      if (response.status === 403) {
        throw new Error('Invalid Seerr API key.');
      }
      if (!response.ok) {
        throw new Error(`Seerr HTTP ${optionsArg.method || 'GET'} ${url.pathname} failed with ${response.status}: ${text.slice(0, 200)}`);
      }
      return parseJson<TValue>(text);
    } finally {
      clearTimeout(timer);
    }
  }

  private shouldUseLiveApi(): boolean {
    return Boolean(this.endpoint() && this.apiKey() && !this.hasOfflineSource());
  }

  private hasOfflineSource(): boolean {
    return Boolean(this.configArg.client || this.configArg.snapshotProvider || this.configArg.snapshot || this.configArg.rawData !== undefined || this.configArg.rawText !== undefined || this.configArg.entities?.length);
  }

  private requireEndpoint(): IOverseerrEndpoint {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('Seerr live local API requires config.host or config.url.');
    }
    return endpoint;
  }

  private endpoint(): IOverseerrEndpoint | undefined {
    const value = stringValue(this.configArg.url) || stringValue(this.configArg.host);
    if (!value) {
      return undefined;
    }
    const protocol = this.configArg.useTls || this.configArg.ssl || this.configArg.protocol === 'https' ? 'https' : 'http';
    try {
      const url = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `${protocol}://${value}`);
      if (this.configArg.port) {
        url.port = String(this.configArg.port);
      }
      const configuredPath = stringValue(this.configArg.path);
      const path = (configuredPath || url.pathname || '').replace(/\/+$/, '');
      const apiPath = path.endsWith('/api/v1') ? path : `${path || ''}/api/v1`;
      const baseUrl = `${url.origin}${apiPath}`;
      return {
        apiBase: baseUrl,
        origin: url.origin,
        host: url.hostname,
        port: this.configArg.port || (url.port ? Number(url.port) : overseerrProfile.defaultPort),
        protocol: url.protocol === 'https:' ? 'https' : 'http',
      };
    } catch {
      return undefined;
    }
  }

  private apiKey(): string | undefined {
    return stringValue(this.configArg.apiKey) || stringValue(this.configArg.token) || stringValue(this.configArg.api_key);
  }

  private timeoutMs(): number {
    return typeof this.configArg.timeoutMs === 'number' && this.configArg.timeoutMs > 0 ? this.configArg.timeoutMs : 10000;
  }

  private offlineOverseerrSnapshot(errorArg: string): IOverseerrSnapshot {
    const endpoint = this.endpoint();
    return OverseerrMapper.toSnapshot({
      config: this.configArg,
      rawData: {
        device: {
          id: this.configArg.uniqueId || endpoint?.host || overseerrProfile.domain,
          name: this.configArg.name || overseerrDefaultName,
          manufacturer: 'Seerr',
          model: 'Seerr server',
          host: endpoint?.host,
          port: endpoint?.port,
          protocol: endpoint?.protocol,
          configurationUrl: endpoint?.origin,
        },
        entities: [{ id: 'online', name: 'Online', platform: 'binary_sensor', state: false, available: true, attributes: { error: errorArg } }],
        online: false,
        updatedAt: new Date().toISOString(),
        source: 'runtime',
        error: errorArg,
      },
      online: false,
      source: 'runtime',
      error: errorArg,
    }) as IOverseerrSnapshot;
  }
}

const parseJson = <TValue>(textArg: string): TValue => textArg.trim() ? JSON.parse(textArg) as TValue : undefined as TValue;

const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;

const cleanAttributes = (attributesArg: Record<string, unknown> | undefined): Record<string, unknown> | undefined => {
  if (!attributesArg) {
    return undefined;
  }
  const result = Object.fromEntries(Object.entries(attributesArg).filter(([, valueArg]) => valueArg !== undefined));
  return Object.keys(result).length ? result : undefined;
};

const recordValue = (valueArg: unknown): Record<string, unknown> | undefined => valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Record<string, unknown> : undefined;

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() && Number.isFinite(Number(valueArg))) {
    return Number(valueArg);
  }
  return undefined;
};

const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
