import { SimpleLocalClient, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import type { IOmbiConfig } from './ombi.types.js';
import { ombiDefaultName, ombiProfile } from './ombi.types.js';

export class OmbiClient extends SimpleLocalClient<IOmbiConfig> {
  private currentOmbiSnapshot?: ISimpleLocalSnapshot;
  private authorizationHeader?: string;

  constructor(private readonly configArg: IOmbiConfig) {
    super(ombiProfile, configArg);
  }

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

    if (this.canUseNativeHttp()) {
      try {
        this.currentOmbiSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.currentOmbiSnapshot = this.offlineOmbiSnapshot(errorMessage(errorArg));
      }
      return clone(this.currentOmbiSnapshot);
    }

    this.currentOmbiSnapshot = await super.getSnapshot(forceRefreshArg);
    return clone(this.currentOmbiSnapshot);
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.commandExecutor || this.configArg.client?.execute) {
      return super.execute(requestArg);
    }
    if (!this.canUseNativeHttp()) {
      return super.execute(requestArg);
    }

    try {
      if (requestArg.service === 'submit_movie_request') {
        const name = requiredString(requestArg.data?.name, 'Ombi submit_movie_request requires data.name.');
        const movies = await this.getJson<Array<Record<string, unknown>>>(`Search/movie/${encodeURIComponent(name)}`);
        const movie = movies[0];
        const movieId = movie?.theMovieDbId;
        if (movieId === undefined) {
          throw new Error('No movie found.');
        }
        await this.postRequest('Request/movie', { theMovieDbId: movieId });
        this.currentOmbiSnapshot = undefined;
        return { success: true, data: { name, theMovieDbId: movieId } };
      }

      if (requestArg.service === 'submit_tv_request') {
        const name = requiredString(requestArg.data?.name, 'Ombi submit_tv_request requires data.name.');
        const season = stringValue(requestArg.data?.season) || 'first';
        if (!['first', 'latest', 'all'].includes(season)) {
          throw new Error('Ombi submit_tv_request season must be first, latest, or all.');
        }
        const shows = await this.getJson<Array<Record<string, unknown>>>(`Search/tv/${encodeURIComponent(name)}`);
        const show = shows[0];
        const tvDbId = show?.id ?? show?.tvDbId;
        if (tvDbId === undefined) {
          throw new Error('No TV show found.');
        }
        await this.postRequest('Request/tv', {
          tvDbId,
          latestSeason: season === 'latest',
          requestAll: season === 'all',
          firstSeason: season === 'first',
        });
        this.currentOmbiSnapshot = undefined;
        return { success: true, data: { name, tvDbId, season } };
      }

      if (requestArg.service === 'submit_music_request') {
        const name = requiredString(requestArg.data?.name, 'Ombi submit_music_request requires data.name.');
        const albums = await this.getJson<Array<Record<string, unknown>>>(`Search/music/album/${encodeURIComponent(name)}`);
        const album = albums[0];
        const albumId = album?.foreignAlbumId;
        if (albumId === undefined) {
          throw new Error('No music album found.');
        }
        await this.postRequest('Request/music', { foreignAlbumId: albumId });
        this.currentOmbiSnapshot = undefined;
        return { success: true, data: { name, foreignAlbumId: albumId } };
      }

      return { success: false, error: `Unsupported Ombi service: ${requestArg.service}` };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  private async fetchSnapshot(): Promise<ISimpleLocalSnapshot> {
    const status = await this.getJson<Record<string, unknown>>('Status');
    const movieRequests = await this.getText('Request/movie/total');
    const tvRequests = await this.getText('Request/tv/total');
    const musicRequests = await this.getText('Request/music/total');
    const totalRequests = await this.getJson<Record<string, unknown>>('Request/count');
    const endpoint = this.endpoint();
    const entities: ISimpleLocalEntitySnapshot[] = [
      sensor('movies', 'Movie requests', numberOrText(movieRequests), { icon: 'mdi:movie' }),
      sensor('tv', 'TV show requests', numberOrText(tvRequests), { icon: 'mdi:television-classic' }),
      sensor('music', 'Music album requests', numberOrText(musicRequests), { icon: 'mdi:album' }),
      sensor('pending', 'Pending requests', numberOrText(totalRequests.pending), { icon: 'mdi:clock-alert-outline' }),
      sensor('approved', 'Approved requests', numberOrText(totalRequests.approved), { icon: 'mdi:check' }),
      sensor('available', 'Available requests', numberOrText(totalRequests.available), { icon: 'mdi:download' }),
    ];

    return {
      device: {
        id: this.configArg.uniqueId || `${endpoint.host}:${endpoint.port || 5000}${endpoint.urlbase}`,
        name: this.configArg.name || ombiDefaultName,
        manufacturer: ombiProfile.manufacturer,
        model: ombiProfile.model,
        host: endpoint.host,
        port: endpoint.port,
        protocol: endpoint.protocol,
        configurationUrl: this.rootUrl(),
        attributes: this.configArg.metadata,
      },
      entities,
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'http',
      rawData: { status, movieRequests, tvRequests, musicRequests, totalRequests },
      capabilities: {
        localRead: true,
        localControl: true,
        liveControlRequiresExecutor: false,
      },
    };
  }

  private async postRequest(pathArg: string, bodyArg: Record<string, unknown>): Promise<unknown> {
    const response = await this.request(pathArg, { method: 'POST', body: JSON.stringify(bodyArg), headers: { 'content-type': 'application/json' } });
    const text = await response.text();
    if (!text) {
      return undefined;
    }
    const parsed = JSON.parse(text) as Record<string, unknown>;
    if (parsed.isError) {
      throw new Error(stringValue(parsed.errorMessage) || 'Ombi request failed.');
    }
    return parsed;
  }

  private async getJson<TValue>(pathArg: string): Promise<TValue> {
    const response = await this.request(pathArg, { method: 'GET' });
    const text = await response.text();
    if (!text) {
      return {} as TValue;
    }
    return JSON.parse(text) as TValue;
  }

  private async getText(pathArg: string): Promise<string> {
    const response = await this.request(pathArg, { method: 'GET' });
    return response.text();
  }

  private async request(pathArg: string, initArg: RequestInit): Promise<Response> {
    await this.authenticate();
    const response = await this.fetchWithTimeout(this.apiUrl(pathArg), {
      ...initArg,
      headers: {
        accept: 'application/json, text/plain;q=0.9, */*;q=0.8',
        ...this.authHeaders(),
        ...(initArg.headers as Record<string, string> | undefined),
      },
    });
    if (!response.ok) {
      const text = await response.clone().text();
      throw new Error(`Ombi ${pathArg} failed with HTTP ${response.status}: ${text.slice(0, 160)}`);
    }
    return response;
  }

  private async authenticate(): Promise<void> {
    if (this.apiKey()) {
      this.authorizationHeader = undefined;
      return;
    }
    if (this.configArg.token) {
      this.authorizationHeader = `Bearer ${this.configArg.token}`;
      return;
    }
    if (this.authorizationHeader) {
      return;
    }
    const password = stringValue(this.configArg.password);
    if (!password) {
      throw new Error('Ombi native HTTP requires config.apiKey, config.token, or config.password.');
    }
    const username = requiredString(this.configArg.username, 'Ombi password authentication requires config.username.');
    const response = await this.fetchWithTimeout(this.apiUrl('Token'), {
      method: 'POST',
      headers: { accept: 'application/json', 'content-type': 'application/json', UserName: username },
      body: JSON.stringify({ userName: username, password }),
    });
    const text = await response.text();
    if (!response.ok) {
      throw new Error(`Ombi Token failed with HTTP ${response.status}: ${text.slice(0, 160)}`);
    }
    const token = stringValue((JSON.parse(text) as Record<string, unknown>).access_token);
    if (!token) {
      throw new Error('Ombi Token response did not include access_token.');
    }
    this.authorizationHeader = `Bearer ${token}`;
  }

  private authHeaders(): Record<string, string> {
    const headers: Record<string, string> = {};
    const username = stringValue(this.configArg.username);
    if (username) {
      headers.UserName = username;
    }
    const apiKey = this.apiKey();
    if (apiKey) {
      headers.ApiKey = apiKey;
    } else if (this.authorizationHeader) {
      headers.Authorization = this.authorizationHeader;
    }
    return headers;
  }

  private async fetchWithTimeout(urlArg: URL, initArg: RequestInit): Promise<Response> {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), this.configArg.timeoutMs || 8000);
    try {
      return await globalThis.fetch(urlArg, { ...initArg, signal: controller.signal });
    } finally {
      clearTimeout(timeout);
    }
  }

  private apiUrl(pathArg: string): URL {
    return new URL(`${this.apiBaseUrl()}/${pathArg.replace(/^\/+/, '')}`);
  }

  private apiBaseUrl(): string {
    const endpoint = this.endpoint();
    const portPart = endpoint.port && !((endpoint.protocol === 'https' && endpoint.port === 443) || (endpoint.protocol === 'http' && endpoint.port === 80)) ? `:${endpoint.port}` : '';
    return `${endpoint.protocol}://${endpoint.host}${portPart}${endpoint.urlbase}api/v1`.replace(/\/+$/, '');
  }

  private rootUrl(): string {
    const endpoint = this.endpoint();
    const portPart = endpoint.port && !((endpoint.protocol === 'https' && endpoint.port === 443) || (endpoint.protocol === 'http' && endpoint.port === 80)) ? `:${endpoint.port}` : '';
    return `${endpoint.protocol}://${endpoint.host}${portPart}${endpoint.urlbase}`.replace(/\/+$/, '');
  }

  private canUseNativeHttp(): boolean {
    return Boolean(this.configArg.url || this.configArg.host);
  }

  private endpoint(): { host: string; port?: number; protocol: 'http' | 'https'; urlbase: string } {
    const value = this.configArg.url || this.configArg.host;
    if (!value) {
      throw new Error('Ombi native HTTP requires config.url or config.host.');
    }
    const protocol = this.configArg.useTls || this.configArg.ssl === true ? 'https' : 'http';
    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 path = cleanUrlbase(stringValue(this.configArg.urlbase) || stringValue(this.configArg.urlBase) || stringValue(this.configArg.path) || (url.pathname.includes('/api/v1') ? url.pathname.replace(/api\/v1\/?$/, '') : url.pathname));
    return {
      host: url.hostname,
      port: url.port ? Number(url.port) : this.configArg.port || (url.protocol === 'https:' ? 443 : 5000),
      protocol: url.protocol === 'https:' ? 'https' : 'http',
      urlbase: path,
    };
  }

  private offlineOmbiSnapshot(errorArg: string): ISimpleLocalSnapshot {
    const endpoint = this.canUseNativeHttp() ? this.endpoint() : undefined;
    return {
      device: {
        id: this.configArg.uniqueId || (endpoint ? `${endpoint.host}:${endpoint.port || 5000}` : this.configArg.name || ombiProfile.domain),
        name: this.configArg.name || ombiDefaultName,
        manufacturer: ombiProfile.manufacturer,
        model: ombiProfile.model,
        host: endpoint?.host,
        port: endpoint?.port,
        protocol: endpoint?.protocol || 'http',
        configurationUrl: endpoint ? this.rootUrl() : undefined,
      },
      entities: [{ id: 'online', name: 'Online', platform: 'binary_sensor', state: false, available: true, attributes: { source: 'runtime' } }],
      online: false,
      updatedAt: new Date().toISOString(),
      source: 'runtime',
      error: errorArg,
    };
  }

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

const sensor = (idArg: string, nameArg: string, stateArg: unknown, attributesArg: Record<string, unknown>): ISimpleLocalEntitySnapshot => ({
  id: idArg,
  name: nameArg,
  platform: 'sensor',
  state: stateArg,
  available: true,
  attributes: { ...attributesArg, key: idArg },
});

const cleanUrlbase = (valueArg: string | undefined): string => {
  const value = (valueArg || '').trim().replace(/^\/+|\/+$/g, '');
  return value ? `/${value}/` : '/';
};

const numberOrText = (valueArg: unknown): string | number => {
  if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
    return valueArg;
  }
  if (typeof valueArg === 'string' && valueArg.trim() && Number.isFinite(Number(valueArg))) {
    return Number(valueArg);
  }
  return String(valueArg ?? '');
};

const requiredString = (valueArg: unknown, messageArg: string): string => {
  const value = stringValue(valueArg);
  if (!value) {
    throw new Error(messageArg);
  }
  return value;
};

const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;

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

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