import { SimpleLocalClient } from '../../core/index.js';
import { InfluxdbMapper } from './influxdb.mapper.js';
import type {
  IInfluxdbConfig,
  IInfluxdbPoint,
  IInfluxdbQueryConfig,
  IInfluxdbQuerySample,
  IInfluxdbRawHttpData,
  IInfluxdbRefreshResult,
  IInfluxdbRepositoryInfo,
  IInfluxdbSnapshot,
  IInfluxdbWriteResult,
  TInfluxdbApiVersion,
  TInfluxdbQueryLanguage,
  TInfluxdbRawData,
} from './influxdb.types.js';
import { influxdbDefaultName, influxdbDefaultPort, influxdbDefaultTimeoutMs, influxdbProfile } from './influxdb.types.js';

interface IInfluxdbV1Series {
  name?: string;
  columns?: string[];
  values?: unknown[][];
}

interface IInfluxdbV1Result {
  error?: string;
  series?: IInfluxdbV1Series[];
}

interface IInfluxdbV1Response {
  error?: string;
  results?: IInfluxdbV1Result[];
}

interface IInfluxdbV2BucketResponse {
  buckets?: Array<{
    id?: string;
    name?: string;
    orgID?: string;
  }>;
}

interface IInfluxdbWriteOptions {
  database?: string;
  bucket?: string;
  organization?: string;
  precision?: 'ns' | 'us' | 'ms' | 's';
}

export class InfluxdbClient extends SimpleLocalClient<IInfluxdbConfig> {
  private influxCurrentSnapshot?: IInfluxdbSnapshot;

  constructor(private readonly configArg: IInfluxdbConfig) {
    super(influxdbProfile, configArg);
  }

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

    if ((!forceRefreshArg && this.configArg.snapshot) || this.configArg.client || this.configArg.snapshotProvider) {
      this.influxCurrentSnapshot = await super.getSnapshot(forceRefreshArg) as IInfluxdbSnapshot;
      return clone(this.influxCurrentSnapshot);
    }

    if (this.hasHttpEndpoint()) {
      try {
        this.influxCurrentSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.influxCurrentSnapshot = this.influxOfflineSnapshot(errorMessage(errorArg));
      }
      return clone(this.influxCurrentSnapshot);
    }

    this.influxCurrentSnapshot = await super.getSnapshot(forceRefreshArg) as IInfluxdbSnapshot;
    return clone(this.influxCurrentSnapshot);
  }

  public async refresh(): Promise<IInfluxdbRefreshResult> {
    this.influxCurrentSnapshot = undefined;
    const snapshot = await this.getSnapshot(true);
    const success = snapshot.online && !snapshot.error;
    return { success, snapshot, error: success ? undefined : snapshot.error, data: { source: snapshot.source } };
  }

  public async getRawData(): Promise<TInfluxdbRawData> {
    return (await this.fetchSnapshot()).rawData || null;
  }

  public async query(queryArg: string, optionsArg: { database?: string; language?: TInfluxdbQueryLanguage } = {}): Promise<Record<string, unknown>[]> {
    const language = optionsArg.language || (this.apiVersion() === '2' ? 'flux' : 'influxql');
    return language === 'flux' ? this.queryFlux(queryArg) : this.queryInfluxql(queryArg, optionsArg.database);
  }

  public async write(pointsArg: string | IInfluxdbPoint | IInfluxdbPoint[], optionsArg: IInfluxdbWriteOptions = {}): Promise<IInfluxdbWriteResult> {
    const apiVersion = this.apiVersion();
    const body = typeof pointsArg === 'string' ? pointsArg : ensureArray(pointsArg).map(pointToLineProtocol).join('\n');
    const lineCount = body.split(/\r?\n/).filter((lineArg) => lineArg.trim()).length;
    if (!body.trim()) {
      throw new Error('InfluxDB write requires at least one line protocol record.');
    }

    if (apiVersion === '2') {
      const bucket = optionsArg.bucket || this.bucket();
      const organization = optionsArg.organization || this.organization();
      if (!bucket || !organization) {
        throw new Error('InfluxDB v2 write requires bucket and organization/org.');
      }
      await this.requestText('/api/v2/write', {
        method: 'POST',
        headers: { 'content-type': 'text/plain; charset=utf-8' },
        body,
      }, {
        org: organization,
        bucket,
        precision: optionsArg.precision || this.configArg.precision,
      });
      return { success: true, lineCount, apiVersion, repository: bucket };
    }

    const database = optionsArg.database || this.database();
    if (!database) {
      throw new Error('InfluxDB v1 write requires database/dbName/db_name.');
    }
    await this.requestText('/write', {
      method: 'POST',
      headers: { 'content-type': 'text/plain; charset=utf-8' },
      body,
    }, {
      db: database,
      precision: optionsArg.precision || this.configArg.precision,
    });
    return { success: true, lineCount, apiVersion, repository: database };
  }

  public async destroy(): Promise<void> {
    await super.destroy();
  }

  private async fetchSnapshot(): Promise<IInfluxdbSnapshot> {
    const apiVersion = this.apiVersion();
    const repositories = apiVersion === '2' ? await this.listBuckets() : await this.listDatabases();
    const queries = await this.readConfiguredQueries(apiVersion);
    const repositoryLabel = apiVersion === '2' ? 'Buckets' : 'Databases';
    const rawData: IInfluxdbRawHttpData = {
      device: {
        id: this.configArg.uniqueId || this.endpointId(),
        name: this.configArg.name || influxdbDefaultName,
        manufacturer: influxdbProfile.manufacturer || influxdbDefaultName,
        model: influxdbProfile.model || influxdbDefaultName,
        host: this.host(),
        port: this.port(),
        protocol: this.useTls() ? 'https' : 'http',
      },
      apiVersion,
      repositories,
      queries,
      entities: [
        { id: 'online', name: 'Online', platform: 'binary_sensor', state: true, attributes: { apiVersion } },
        { id: 'repository_count', name: repositoryLabel, platform: 'sensor', state: repositories.length, attributes: { apiVersion, repositories: repositories.map((repositoryArg) => repositoryArg.name) } },
        ...queries.map((queryArg, indexArg) => ({
          id: InfluxdbMapper.slug(queryArg.name || `query_${indexArg + 1}`),
          name: queryArg.name,
          platform: 'sensor' as const,
          state: queryArg.value,
          unit: queryArg.unit,
          attributes: {
            apiVersion,
            language: queryArg.language,
            repository: queryArg.repository,
            query: queryArg.query,
          },
        })),
      ],
    };

    const snapshot = InfluxdbMapper.toSnapshot({
      config: { ...this.configArg, entities: rawData.entities },
      rawData: { apiVersion, repositories, queries },
      online: true,
      source: 'http',
    }) as IInfluxdbSnapshot;
    snapshot.rawData = rawData;
    return snapshot;
  }

  private async listDatabases(): Promise<IInfluxdbRepositoryInfo[]> {
    const points = await this.queryInfluxql('SHOW DATABASES;');
    return points
      .map((pointArg) => stringValue(pointArg.name))
      .filter((nameArg): nameArg is string => Boolean(nameArg))
      .map((nameArg) => ({ name: nameArg, kind: 'database' }));
  }

  private async listBuckets(): Promise<IInfluxdbRepositoryInfo[]> {
    if (!this.configArg.token) {
      throw new Error('InfluxDB v2 bucket discovery requires token.');
    }
    const response = await this.requestJson<IInfluxdbV2BucketResponse>('/api/v2/buckets', { method: 'GET' }, { org: this.organization() });
    return (response.buckets || [])
      .filter((bucketArg) => Boolean(bucketArg.name))
      .map((bucketArg) => ({ name: bucketArg.name!, kind: 'bucket', id: bucketArg.id, organization: bucketArg.orgID || this.organization() }));
  }

  private async readConfiguredQueries(apiVersionArg: TInfluxdbApiVersion): Promise<IInfluxdbQuerySample[]> {
    const configs = apiVersionArg === '2' ? this.fluxQueryConfigs() : this.influxqlQueryConfigs();
    const samples: IInfluxdbQuerySample[] = [];
    for (const [index, queryConfig] of configs.entries()) {
      const query = apiVersionArg === '2' ? this.fluxQueryText(queryConfig) : this.influxqlQueryText(queryConfig);
      if (!query) {
        continue;
      }
      const records = apiVersionArg === '2'
        ? await this.queryFlux(query, { organization: queryConfig.organization || queryConfig.org })
        : await this.queryInfluxql(query, queryConfig.database || queryConfig.dbName || queryConfig.db_name || this.database());
      const firstRecord = records[0] || {};
      samples.push({
        name: queryConfig.name || `Query ${index + 1}`,
        query,
        language: apiVersionArg === '2' ? 'flux' : 'influxql',
        value: firstValue(firstRecord),
        repository: apiVersionArg === '2' ? queryConfig.bucket || this.bucket() : queryConfig.database || queryConfig.dbName || queryConfig.db_name || this.database(),
        unit: queryConfig.unitOfMeasurement || queryConfig.unit_of_measurement,
        raw: firstRecord,
      });
    }
    return samples;
  }

  private async queryInfluxql(queryArg: string, databaseArg?: string): Promise<Record<string, unknown>[]> {
    const response = await this.requestJson<IInfluxdbV1Response>('/query', { method: 'GET' }, { db: databaseArg, q: queryArg });
    if (response.error) {
      throw new Error(`InfluxDB query failed: ${response.error}`);
    }
    const points: Record<string, unknown>[] = [];
    for (const result of response.results || []) {
      if (result.error) {
        throw new Error(`InfluxDB query failed: ${result.error}`);
      }
      for (const series of result.series || []) {
        const columns = series.columns || [];
        for (const values of series.values || []) {
          const point: Record<string, unknown> = { measurement: series.name };
          for (const [index, column] of columns.entries()) {
            point[column] = values[index];
          }
          points.push(point);
        }
      }
    }
    return points;
  }

  private async queryFlux(queryArg: string, optionsArg: { organization?: string } = {}): Promise<Record<string, unknown>[]> {
    if (!this.configArg.token) {
      throw new Error('InfluxDB v2 query requires token.');
    }
    const text = await this.requestText('/api/v2/query', {
      method: 'POST',
      headers: { accept: 'application/csv', 'content-type': 'application/json' },
      body: JSON.stringify({ query: queryArg }),
    }, { org: optionsArg.organization || this.organization() });
    return parseFluxRecords(text);
  }

  private async requestJson<TResponse>(pathArg: string, initArg: RequestInit, paramsArg: Record<string, string | number | undefined> = {}): Promise<TResponse> {
    const text = await this.requestText(pathArg, { ...initArg, headers: { accept: 'application/json', ...headersValue(initArg.headers) } }, paramsArg);
    try {
      return JSON.parse(text || '{}') as TResponse;
    } catch (errorArg) {
      throw new Error(`InfluxDB returned invalid JSON from ${pathArg}: ${errorMessage(errorArg)}`);
    }
  }

  private async requestText(pathArg: string, initArg: RequestInit, paramsArg: Record<string, string | number | undefined> = {}): Promise<string> {
    const url = this.url(pathArg, paramsArg);
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), this.configArg.timeoutMs || influxdbDefaultTimeoutMs);
    try {
      const response = await globalThis.fetch(url, {
        ...initArg,
        headers: this.headers(initArg.headers),
        signal: controller.signal,
      });
      const text = await response.text();
      if (!response.ok) {
        throw new Error(`InfluxDB HTTP ${response.status} from ${pathArg}: ${text.slice(0, 200)}`);
      }
      return text;
    } catch (errorArg) {
      throw new Error(`InfluxDB request to ${url} failed: ${errorMessage(errorArg)}`);
    } finally {
      clearTimeout(timeout);
    }
  }

  private headers(headersArg: HeadersInit | undefined): Record<string, string> {
    const headers = headersValue(headersArg);
    if (this.configArg.token) {
      headers.authorization = `Token ${this.configArg.token}`;
    } else if (this.configArg.username) {
      headers.authorization = `Basic ${Buffer.from(`${this.configArg.username}:${this.configArg.password || ''}`).toString('base64')}`;
    }
    return headers;
  }

  private url(pathArg: string, paramsArg: Record<string, string | number | undefined>): string {
    const base = this.baseUrl();
    const prefix = base.pathname.replace(/\/+$/, '');
    const path = pathArg.startsWith('/') ? pathArg : `/${pathArg}`;
    base.pathname = `${prefix}${path}`.replace(/\/+/g, '/');
    for (const [key, value] of Object.entries(paramsArg)) {
      if (value !== undefined && value !== '') {
        base.searchParams.set(key, String(value));
      }
    }
    return base.toString();
  }

  private baseUrl(): URL {
    const configuredUrl = stringValue(this.configArg.url);
    if (configuredUrl) {
      return new URL(configuredUrl.includes('://') ? configuredUrl : `${this.useTls() ? 'https' : 'http'}://${configuredUrl}`);
    }
    const host = this.configArg.host;
    if (!host) {
      throw new Error('InfluxDB HTTP API requires config.url or config.host.');
    }
    const path = stringValue(this.configArg.path) || '';
    return new URL(`${this.useTls() ? 'https' : 'http'}://${hostForUrl(host)}:${this.port()}${path.startsWith('/') || !path ? path : `/${path}`}`);
  }

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

  private endpointId(): string {
    return `${this.host() || 'influxdb'}:${this.port()}`;
  }

  private host(): string | undefined {
    if (this.configArg.host) {
      return this.configArg.host;
    }
    const configuredUrl = stringValue(this.configArg.url);
    if (!configuredUrl) {
      return undefined;
    }
    try {
      return new URL(configuredUrl.includes('://') ? configuredUrl : `${this.useTls() ? 'https' : 'http'}://${configuredUrl}`).hostname;
    } catch {
      return undefined;
    }
  }

  private port(): number {
    if (this.configArg.port) {
      return this.configArg.port;
    }
    const configuredUrl = stringValue(this.configArg.url);
    if (configuredUrl) {
      try {
        const parsed = new URL(configuredUrl.includes('://') ? configuredUrl : `${this.useTls() ? 'https' : 'http'}://${configuredUrl}`);
        if (parsed.port) {
          return Number(parsed.port);
        }
      } catch {}
    }
    return influxdbDefaultPort;
  }

  private useTls(): boolean {
    return Boolean(this.configArg.useTls || this.configArg.ssl);
  }

  private apiVersion(): TInfluxdbApiVersion {
    return String(this.configArg.apiVersion || this.configArg.api_version || '1') === '2' ? '2' : '1';
  }

  private database(): string | undefined {
    return stringValue(this.configArg.database) || stringValue(this.configArg.dbName) || stringValue(this.configArg.db_name);
  }

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

  private organization(): string | undefined {
    return stringValue(this.configArg.organization) || stringValue(this.configArg.org);
  }

  private influxqlQueryConfigs(): IInfluxdbQueryConfig[] {
    return arrayValue<IInfluxdbQueryConfig>(this.configArg.queries);
  }

  private fluxQueryConfigs(): IInfluxdbQueryConfig[] {
    return [
      ...arrayValue<IInfluxdbQueryConfig>(this.configArg.queriesFlux),
      ...arrayValue<IInfluxdbQueryConfig>(this.configArg.queries_flux),
    ];
  }

  private influxqlQueryText(queryConfigArg: IInfluxdbQueryConfig): string | undefined {
    if (queryConfigArg.query) {
      return queryConfigArg.query;
    }
    if (!queryConfigArg.measurement || !queryConfigArg.where) {
      return undefined;
    }
    const groupFunction = queryConfigArg.groupFunction || queryConfigArg.group_function || 'mean';
    const field = queryConfigArg.field || 'value';
    return `select ${groupFunction}(${field}) as value from ${queryConfigArg.measurement} where ${queryConfigArg.where}`;
  }

  private fluxQueryText(queryConfigArg: IInfluxdbQueryConfig): string | undefined {
    if (!queryConfigArg.query) {
      return undefined;
    }
    if (/\bfrom\s*\(/i.test(queryConfigArg.query)) {
      return queryConfigArg.query;
    }
    const bucket = queryConfigArg.bucket || this.bucket();
    if (!bucket) {
      throw new Error('InfluxDB Flux query requires bucket.');
    }
    const imports = (queryConfigArg.imports || []).map((importArg) => `import "${importArg}"`).join(' ');
    const rangeStart = queryConfigArg.rangeStart || queryConfigArg.range_start || '-15m';
    const rangeStop = queryConfigArg.rangeStop || queryConfigArg.range_stop || 'now()';
    const groupFunction = queryConfigArg.groupFunction || queryConfigArg.group_function;
    const postfix = groupFunction ? `|> ${groupFunction}(column: "_value")` : '|> limit(n: 1)';
    return `${imports}${imports ? ' ' : ''}from(bucket:"${bucket}") |> range(start: ${rangeStart}, stop: ${rangeStop}) |> ${queryConfigArg.query} ${postfix}`;
  }

  private influxOfflineSnapshot(errorArg: string): IInfluxdbSnapshot {
    return InfluxdbMapper.toSnapshot({ config: this.configArg, online: false, source: 'runtime', error: errorArg }) as IInfluxdbSnapshot;
  }
}

const parseFluxRecords = (textArg: string): Record<string, unknown>[] => {
  const trimmed = textArg.trim();
  if (!trimmed) {
    return [];
  }
  if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
    const parsed = JSON.parse(trimmed) as unknown;
    if (Array.isArray(parsed)) {
      return parsed.filter((valueArg): valueArg is Record<string, unknown> => Boolean(valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg)));
    }
    if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
      return [parsed as Record<string, unknown>];
    }
    return [];
  }
  const lines = trimmed.split(/\r?\n/).filter((lineArg) => lineArg.trim() && !lineArg.startsWith('#'));
  if (lines.length < 2) {
    return [];
  }
  const headers = parseCsvLine(lines[0]).map((headerArg, indexArg) => headerArg || `column_${indexArg}`);
  return lines.slice(1).map((lineArg) => {
    const values = parseCsvLine(lineArg);
    const record: Record<string, unknown> = {};
    for (const [index, header] of headers.entries()) {
      record[header] = coerceCsvValue(values[index]);
    }
    return record;
  });
};

const parseCsvLine = (lineArg: string): string[] => {
  const values: string[] = [];
  let current = '';
  let quoted = false;
  for (let index = 0; index < lineArg.length; index += 1) {
    const char = lineArg[index];
    if (char === '"' && lineArg[index + 1] === '"') {
      current += '"';
      index += 1;
      continue;
    }
    if (char === '"') {
      quoted = !quoted;
      continue;
    }
    if (char === ',' && !quoted) {
      values.push(current);
      current = '';
      continue;
    }
    current += char;
  }
  values.push(current);
  return values;
};

const coerceCsvValue = (valueArg: string | undefined): unknown => {
  if (valueArg === undefined || valueArg === '') {
    return undefined;
  }
  if (valueArg === 'true') {
    return true;
  }
  if (valueArg === 'false') {
    return false;
  }
  if (Number.isFinite(Number(valueArg))) {
    return Number(valueArg);
  }
  return valueArg;
};

const firstValue = (recordArg: Record<string, unknown>): unknown => {
  if ('value' in recordArg) {
    return recordArg.value;
  }
  if ('_value' in recordArg) {
    return recordArg._value;
  }
  for (const [key, value] of Object.entries(recordArg)) {
    if (!['time', '_time', 'measurement', 'result', 'table'].includes(key) && isPrimitive(value)) {
      return value;
    }
  }
  return undefined;
};

const pointToLineProtocol = (pointArg: IInfluxdbPoint): string => {
  const tags = Object.entries(pointArg.tags || {})
    .map(([key, value]) => `${escapeKey(key)}=${escapeKey(String(value))}`)
    .join(',');
  const fields = Object.entries(pointArg.fields)
    .map(([key, value]) => `${escapeKey(key)}=${fieldValue(value)}`)
    .join(',');
  const timestamp = pointArg.time instanceof Date ? pointArg.time.getTime() * 1_000_000 : pointArg.time;
  return `${escapeKey(pointArg.measurement)}${tags ? `,${tags}` : ''} ${fields}${timestamp ? ` ${timestamp}` : ''}`;
};

const fieldValue = (valueArg: string | number | boolean): string => {
  if (typeof valueArg === 'string') {
    return `"${valueArg.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
  }
  if (typeof valueArg === 'boolean') {
    return valueArg ? 'true' : 'false';
  }
  return Number.isInteger(valueArg) ? `${valueArg}i` : String(valueArg);
};

const escapeKey = (valueArg: string): string => valueArg.replace(/([ ,=])/g, '\\$1');

const headersValue = (headersArg: HeadersInit | undefined): Record<string, string> => {
  if (!headersArg) {
    return {};
  }
  if (headersArg instanceof Headers) {
    return Object.fromEntries(headersArg.entries());
  }
  if (Array.isArray(headersArg)) {
    return Object.fromEntries(headersArg);
  }
  return { ...headersArg };
};

const ensureArray = <TValue>(valueArg: TValue | TValue[]): TValue[] => Array.isArray(valueArg) ? valueArg : [valueArg];

const arrayValue = <TValue>(valueArg: unknown): TValue[] => Array.isArray(valueArg) ? valueArg as TValue[] : [];

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

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

const isPrimitive = (valueArg: unknown): valueArg is string | number | boolean | null => valueArg === null || ['string', 'number', 'boolean'].includes(typeof valueArg);

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);
