import { PiHoleMapper } from './pi_hole.mapper.js';
import type {
  IPiHoleClientCommand,
  IPiHoleCommandResult,
  IPiHoleConfig,
  IPiHoleRawData,
  IPiHoleSnapshot,
  IPiHoleV5Summary,
  IPiHoleV5Versions,
  IPiHoleV6BlockingStatus,
  IPiHoleV6InfoVersionResponse,
  IPiHoleV6Summary,
  TPiHoleApiVersion,
} from './pi_hole.types.js';
import { piHoleDefaultLocation, piHoleDefaultPort, piHoleDefaultTimeoutMs } from './pi_hole.types.js';

export class PiHoleApiError extends Error {}
export class PiHoleConnectionError extends PiHoleApiError {}
export class PiHoleAuthorizationError extends PiHoleApiError {}

export class PiHoleClient {
  private currentSnapshot?: IPiHoleSnapshot;
  private sessionId?: string;
  private csrfToken?: string;
  private sessionValidUntil?: number;

  constructor(private readonly config: IPiHoleConfig) {}

  public async getSnapshot(): Promise<IPiHoleSnapshot> {
    if (this.hasManualData()) {
      this.currentSnapshot = PiHoleMapper.toSnapshot({
        config: this.config,
        source: this.config.snapshot ? 'snapshot' : 'manual',
        online: this.config.snapshot?.online ?? this.config.online ?? true,
      });
      return this.cloneSnapshot(this.currentSnapshot);
    }

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

    this.currentSnapshot = this.offlineSnapshot('No Pi-hole HTTP endpoint or snapshot/manual data is configured.');
    return this.cloneSnapshot(this.currentSnapshot);
  }

  public async refresh(): Promise<{ success: boolean; snapshot: IPiHoleSnapshot; error?: string; data?: Record<string, unknown> }> {
    if (this.hasManualData()) {
      const snapshot = await this.getSnapshot();
      return { success: true, snapshot, data: { source: snapshot.source, apiVersion: snapshot.apiVersion } };
    }

    if (!this.config.host) {
      const snapshot = await this.getSnapshot();
      return {
        success: false,
        snapshot,
        error: 'Pi-hole refresh requires a configured HTTP endpoint or snapshot/manual data.',
      };
    }

    try {
      const snapshot = await this.fetchSnapshot();
      this.currentSnapshot = snapshot;
      return { success: true, snapshot: this.cloneSnapshot(snapshot), data: { source: 'http', apiVersion: snapshot.apiVersion } };
    } catch (errorArg) {
      const error = this.errorMessage(errorArg);
      const snapshot = this.offlineSnapshot(error);
      this.currentSnapshot = snapshot;
      return { success: false, snapshot: this.cloneSnapshot(snapshot), error };
    }
  }

  public async ping(): Promise<boolean> {
    if (this.hasManualData()) {
      return true;
    }
    if (!this.config.host) {
      return false;
    }
    return (await this.refresh()).success;
  }

  public async sendCommand(commandArg: IPiHoleClientCommand): Promise<IPiHoleCommandResult> {
    if (this.config.commandExecutor) {
      return this.commandResult(await this.config.commandExecutor(commandArg), commandArg);
    }

    if (commandArg.type === 'refresh') {
      const result = await this.refresh();
      return { success: result.success, error: result.error, data: result.snapshot };
    }

    if (!this.config.host) {
      return {
        success: false,
        error: 'Pi-hole live commands require config.host or commandExecutor; snapshot-only commands are not reported as successful.',
        data: { command: commandArg },
      };
    }

    const apiVersion = commandArg.apiVersion || this.currentSnapshot?.apiVersion || this.config.apiVersion || await this.detectApiVersion();
    if (commandArg.type === 'enable' || commandArg.type === 'disable') {
      const response = apiVersion === 6
        ? await this.setV6Blocking(commandArg.type === 'enable', commandArg.durationSeconds)
        : await this.setV5Blocking(commandArg.type === 'enable', commandArg.durationSeconds);
      return { success: true, data: { command: { ...commandArg, apiVersion }, response } };
    }

    return { success: false, error: `Unsupported Pi-hole command: ${commandArg.type}`, data: { command: commandArg } };
  }

  public async destroy(): Promise<void> {
    if (this.sessionId) {
      await this.logoutV6().catch(() => undefined);
    }
  }

  public async fetchSnapshot(): Promise<IPiHoleSnapshot> {
    if (this.config.apiVersion === 5) {
      return this.fetchV5Snapshot();
    }
    if (this.config.apiVersion === 6) {
      return this.fetchV6Snapshot();
    }

    try {
      return await this.fetchV6Snapshot();
    } catch (errorArg) {
      if (errorArg instanceof PiHoleAuthorizationError) {
        throw errorArg;
      }
      return this.fetchV5Snapshot();
    }
  }

  private async fetchV5Snapshot(): Promise<IPiHoleSnapshot> {
    const [summary, versions] = await Promise.all([
      this.requestV5<IPiHoleV5Summary | unknown[]>('summaryRaw'),
      this.requestV5<IPiHoleV5Versions>('versions'),
    ]);

    if (!summary || Array.isArray(summary) || typeof summary !== 'object') {
      throw new PiHoleAuthorizationError('Pi-hole v5 returned an unauthenticated or invalid summary response.');
    }
    if ('error' in summary) {
      throw new PiHoleApiError(`Pi-hole v5 summary returned an error: ${JSON.stringify(summary.error)}`);
    }

    return PiHoleMapper.toSnapshot({
      config: this.config,
      rawData: { v5Summary: summary as IPiHoleV5Summary, v5Versions: versions },
      online: true,
      source: 'http',
      apiVersion: 5,
    });
  }

  private async fetchV6Snapshot(): Promise<IPiHoleSnapshot> {
    await this.ensureV6Auth();
    const [summary, blocking, versions] = await Promise.all([
      this.requestV6<IPiHoleV6Summary>('/api/stats/summary'),
      this.requestV6<IPiHoleV6BlockingStatus>('/api/dns/blocking'),
      this.requestV6<IPiHoleV6InfoVersionResponse>('/api/info/version'),
    ]);

    return PiHoleMapper.toSnapshot({
      config: this.config,
      rawData: { v6Summary: summary, v6Blocking: blocking, v6Versions: versions },
      online: true,
      source: 'http',
      apiVersion: 6,
    });
  }

  private async detectApiVersion(): Promise<TPiHoleApiVersion> {
    if (this.config.apiVersion) {
      return this.config.apiVersion;
    }
    if (this.currentSnapshot?.apiVersion) {
      return this.currentSnapshot.apiVersion;
    }
    const snapshot = await this.fetchSnapshot();
    this.currentSnapshot = snapshot;
    return snapshot.apiVersion || 6;
  }

  private async setV5Blocking(enabledArg: boolean, durationSecondsArg?: number): Promise<unknown> {
    const apiKey = this.apiKey();
    if (!apiKey) {
      throw new PiHoleAuthorizationError('Pi-hole v5 enable/disable requires apiKey.');
    }
    const query = enabledArg ? 'enable=True' : `disable=${durationSecondsArg ?? true}`;
    const response = await this.requestV5<unknown>(query, false);
    this.currentSnapshot = await this.fetchV5Snapshot().catch(() => this.currentSnapshot);
    return response;
  }

  private async setV6Blocking(enabledArg: boolean, durationSecondsArg?: number): Promise<unknown> {
    await this.ensureV6Auth();
    const response = await this.requestV6<unknown>('/api/dns/blocking', {
      method: 'POST',
      body: JSON.stringify({ blocking: enabledArg, timer: enabledArg ? null : durationSecondsArg ?? null }),
      headers: { 'content-type': 'application/json' },
    });
    this.currentSnapshot = await this.fetchV6Snapshot().catch(() => this.currentSnapshot);
    return response;
  }

  private async requestV5<TResponse>(queryArg: string, appendAuthArg = true): Promise<TResponse> {
    const auth = this.apiKey();
    const query = appendAuthArg || auth ? `${queryArg}&auth=${encodeURIComponent(auth || '')}` : queryArg;
    const url = new URL(this.v5ApiUrl());
    url.search = query;
    return this.requestJson<TResponse>(String(url), { method: 'GET' });
  }

  private async requestV6<TResponse>(pathArg: string, initArg: RequestInit = {}, retryArg = true): Promise<TResponse> {
    const headers: Record<string, string> = {
      ...(initArg.headers as Record<string, string> | undefined),
    };
    if (this.sessionId) {
      headers['X-FTL-SID'] = this.sessionId;
      if (this.csrfToken) {
        headers['X-FTL-CSRF'] = this.csrfToken;
      }
    }

    try {
      return await this.requestJson<TResponse>(`${this.baseUrl()}${pathArg}`, { ...initArg, headers });
    } catch (errorArg) {
      if (retryArg && errorArg instanceof PiHoleAuthorizationError && this.sessionId) {
        this.sessionId = undefined;
        this.csrfToken = undefined;
        this.sessionValidUntil = undefined;
        await this.ensureV6Auth();
        return this.requestV6<TResponse>(pathArg, initArg, false);
      }
      throw errorArg;
    }
  }

  private async ensureV6Auth(): Promise<void> {
    const password = this.apiKey();
    if (!password) {
      return;
    }
    if (this.sessionId && this.sessionValidUntil && Date.now() < this.sessionValidUntil) {
      return;
    }
    await this.authenticateV6(password);
  }

  private async authenticateV6(passwordArg: string): Promise<void> {
    const response = await this.requestJson<{ session?: { valid?: boolean; sid?: string; csrf?: string; validity?: number } }>(`${this.baseUrl()}/api/auth`, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ password: passwordArg }),
    });
    const session = response.session;
    if (!session?.valid || !session.sid) {
      throw new PiHoleAuthorizationError('Pi-hole v6 authentication did not return a valid session.');
    }
    this.sessionId = session.sid;
    this.csrfToken = session.csrf;
    this.sessionValidUntil = Date.now() + Math.max((session.validity || 300) - 5, 1) * 1000;
  }

  private async logoutV6(): Promise<void> {
    if (!this.sessionId) {
      return;
    }
    await this.requestJson<unknown>(`${this.baseUrl()}/api/auth`, {
      method: 'DELETE',
      headers: { 'X-FTL-SID': this.sessionId },
    }).catch(() => undefined);
    this.sessionId = undefined;
    this.csrfToken = undefined;
    this.sessionValidUntil = undefined;
  }

  private async requestJson<TResponse>(urlArg: string, initArg: RequestInit): Promise<TResponse> {
    let response: Response;
    try {
      response = await globalThis.fetch(urlArg, {
        ...initArg,
        signal: AbortSignal.timeout(this.config.timeoutMs || piHoleDefaultTimeoutMs),
      });
    } catch (errorArg) {
      throw new PiHoleConnectionError(`Connection to ${urlArg} failed: ${this.errorMessage(errorArg)}`);
    }

    const text = await response.text();
    if (response.status === 401) {
      throw new PiHoleAuthorizationError('Pi-hole authentication failed.');
    }
    if (!response.ok) {
      throw new PiHoleApiError(`Pi-hole request ${urlArg} failed with HTTP ${response.status}: ${text}`);
    }
    if (!text) {
      return {} as TResponse;
    }
    try {
      return JSON.parse(text) as TResponse;
    } catch (errorArg) {
      throw new PiHoleConnectionError(`Unable to parse Pi-hole response from ${urlArg}: ${this.errorMessage(errorArg)}`);
    }
  }

  private offlineSnapshot(errorArg: string): IPiHoleSnapshot {
    return PiHoleMapper.toSnapshot({
      config: this.config,
      online: false,
      source: 'runtime',
      error: errorArg,
    });
  }

  private hasManualData(): boolean {
    return Boolean(
      this.config.snapshot
      || this.config.rawData
      || this.config.v5Summary
      || this.config.v5Versions
      || this.config.v6Summary
      || this.config.v6Blocking
      || this.config.v6Versions
      || this.config.status !== undefined
      || this.config.statistics
      || this.config.versions
    );
  }

  private v5ApiUrl(): string {
    return `${this.baseUrl()}/${this.location()}/api.php`;
  }

  private baseUrl(): string {
    if (!this.config.host) {
      throw new PiHoleConnectionError('Pi-hole host is required for HTTP API access.');
    }
    const protocol = this.config.ssl ? 'https' : 'http';
    const port = this.config.port || (this.config.ssl ? 443 : piHoleDefaultPort);
    const defaultPort = protocol === 'https' ? 443 : 80;
    return `${protocol}://${this.hostForUrl(this.config.host)}${port === defaultPort ? '' : `:${port}`}`;
  }

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

  private location(): string {
    const location = this.config.location || piHoleDefaultLocation;
    return location.trim().replace(/^\/+|\/+$/g, '') || piHoleDefaultLocation;
  }

  private apiKey(): string | undefined {
    return this.stringValue(this.config.apiKey) || this.stringValue(this.config.password);
  }

  private stringValue(valueArg: unknown): string | undefined {
    return typeof valueArg === 'string' && valueArg ? valueArg : undefined;
  }

  private commandResult(resultArg: unknown, commandArg: IPiHoleClientCommand): IPiHoleCommandResult {
    if (this.isCommandResult(resultArg)) {
      return resultArg;
    }
    return { success: true, data: resultArg ?? { command: commandArg } };
  }

  private isCommandResult(valueArg: unknown): valueArg is IPiHoleCommandResult {
    return typeof valueArg === 'object' && valueArg !== null && 'success' in valueArg;
  }

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

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