import {
  ZabbixConfig,
  ZabbixHost,
  ZabbixHostGroup,
  ZabbixTemplate,
  ZabbixHostCreateParams,
  ZabbixHostUpdateParams,
  ZabbixRpcResponse,
  ZabbixProblem,
  ZabbixEvent,
} from '@/lib/types/zabbix';

export type { ZabbixHostTag } from '@/lib/types/zabbix';

export class ZabbixClient {
  private config: ZabbixConfig;
  private rpcId = 0;

  constructor(config: ZabbixConfig) {
    this.config = config;
  }

  /**
   * Make a JSON-RPC 2.0 call to the Zabbix API.
   * Auth is stateless: Bearer token in Authorization header (Zabbix 6.0+ API token).
   */
  private async rpc<T>(method: string, params: Record<string, any>): Promise<T> {
    const id = ++this.rpcId;
    const url = `${this.config.apiUrl.replace(/\/$/, '')}/api_jsonrpc.php`;

    const body = JSON.stringify({
      jsonrpc: '2.0',
      method,
      params,
      id,
    });

    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.config.apiToken}`,
      },
      body,
    });

    if (!response.ok) {
      const text = await response.text();
      throw new Error(`Zabbix HTTP ${response.status}: ${text.substring(0, 200)}`);
    }

    const data: ZabbixRpcResponse<T> = await response.json();

    if (data.error) {
      throw new Error(
        `Zabbix RPC error (${data.error.code}): ${data.error.message}` +
        (data.error.data ? ` — ${data.error.data}` : '')
      );
    }

    return data.result as T;
  }

  /**
   * Get or create a host group by name. Returns the groupid.
   */
  async ensureHostGroup(name: string): Promise<string> {
    const existing = await this.rpc<ZabbixHostGroup[]>('hostgroup.get', {
      output: ['groupid', 'name'],
      filter: { name: [name] },
    });

    if (existing.length > 0) {
      return existing[0].groupid;
    }

    const created = await this.rpc<{ groupids: string[] }>('hostgroup.create', {
      name,
    });

    return created.groupids[0];
  }

  /**
   * Find a template by exact or partial host name. Returns the first match or null.
   */
  async findTemplate(name: string): Promise<ZabbixTemplate | null> {
    const results = await this.rpc<ZabbixTemplate[]>('template.get', {
      output: ['templateid', 'host', 'name'],
      search: { host: name },
      searchByAny: false,
    });

    return results.length > 0 ? results[0] : null;
  }

  /**
   * Find a host by its technical name (host field), with a fallback search by
   * display name. The fallback handles legacy hosts created before hostname
   * sanitization was introduced (their host == name == full display string).
   */
  async findHostByName(hostname: string, displayName?: string): Promise<ZabbixHost | null> {
    const results = await this.rpc<ZabbixHost[]>('host.get', {
      output: ['hostid', 'host', 'name', 'status'],
      filter: { host: [hostname] },
    });
    if (results.length > 0) return results[0];

    // Fallback: search by display name (catches pre-sanitization legacy hosts)
    if (displayName && displayName !== hostname) {
      const byName = await this.rpc<ZabbixHost[]>('host.get', {
        output: ['hostid', 'host', 'name', 'status'],
        filter: { name: [displayName] },
      });
      if (byName.length > 0) return byName[0];
    }

    return null;
  }

  /**
   * Fetch all hosts with full detail: interfaces, tags, macros, groups.
   */
  async getHosts(): Promise<ZabbixHost[]> {
    return this.rpc<ZabbixHost[]>('host.get', {
      output: 'extend',
      selectInterfaces: 'extend',
      selectTags: 'extend',
      selectMacros: 'extend',
      selectGroups: 'extend',
      selectParentTemplates: ['templateid', 'host', 'name'],
    });
  }

  /**
   * Fetch PROBLEM trigger events (value=1) for a set of hostids within a time range.
   * Used to build the local zabbix_events correlation cache.
   */
  async getEvents(params: {
    hostIds: string[];
    from: Date;
    to: Date;
    limit?: number;
  }): Promise<Array<{
    eventid: string;
    objectid: string;
    name: string;
    severity: string;
    clock: string;
    r_eventid: string;
    r_clock: string;
    hosts: Array<{ hostid: string }>;
  }>> {
    // problem.get supports r_clock output; event.get does not
    return this.rpc('problem.get', {
      output: ['eventid', 'objectid', 'name', 'severity', 'clock', 'r_eventid', 'r_clock'],
      time_from: Math.floor(params.from.getTime() / 1000),
      time_till: Math.floor(params.to.getTime() / 1000),
      hostids: params.hostIds,
      selectHosts: ['hostid'],
      recent: false,
      sortfield: 'eventid',
      sortorder: 'DESC',
      limit: params.limit ?? 10000,
    });
  }

  /**
   * Delete one or more hosts by their hostids.
   */
  async deleteHosts(hostids: string[]): Promise<void> {
    await this.rpc<{ hostids: string[] }>('host.delete', hostids);
  }

  /**
   * Get open problems with hosts inline.
   * Only returns problems whose trigger is linked to at least one enabled host.
   * severities: 2=Warning,3=Average,4=High,5=Disaster
   */
  async getOpenProblems(minSeverity = 2): Promise<ZabbixProblem[]> {
    return this.rpc<ZabbixProblem[]>('problem.get', {
      output: 'extend',
      selectAcknowledges: 'extend',
      selectSuppressionData: 'extend',
      severities: [2, 3, 4, 5].filter(s => s >= minSeverity),
      recent: true,
      sortfield: 'eventid',
      sortorder: 'DESC',
    });
  }

  /**
   * Get problems that were resolved within the given window.
   * Uses problem.get with time_from/time_till (filters on problem clock) and
   * r_eventid IS SET (meaning the problem has a recovery event = resolved).
   * Returns problems only — r_clock is the resolution timestamp.
   */
  async getResolvedEvents(from: Date, to: Date): Promise<ZabbixEvent[]> {
    const fromSec = Math.floor(from.getTime() / 1000);
    const toSec = Math.floor(to.getTime() / 1000);

    // event.get with value:1 returns PROBLEM trigger events.
    // time_from/time_till filter on event creation (problem start) time.
    // We then keep only those that have an r_clock (= resolved) within the window.
    const events = await this.rpc<ZabbixEvent[]>('event.get', {
      output: 'extend',
      source: 0,
      object: 0,
      value: 1,
      time_from: fromSec,
      time_till: toSec,
      severities: [2, 3, 4, 5],
      sortfield: 'eventid',
      sortorder: 'DESC',
      limit: 500,
    });

    return events.filter(e => e.r_eventid && e.r_eventid !== '0');
  }

  /**
   * Fetch group names for a specific list of hostids.
   * Returns a Map<hostid, groupName[]> for client name resolution.
   */
  async getHostGroupMap(hostids: string[]): Promise<Map<string, string[]>> {
    if (hostids.length === 0) return new Map();
    const hosts = await this.rpc<Array<{
      hostid: string;
      groups: Array<{ groupid: string; name: string }>;
    }>>('host.get', {
      output: ['hostid'],
      hostids,
      selectGroups: ['groupid', 'name'],
    });

    const map = new Map<string, string[]>();
    for (const h of hosts) {
      map.set(h.hostid, (h.groups ?? []).map((g: { name: string }) => g.name));
    }
    return map;
  }

  /**
   * For a list of triggerids, return a map of triggerid → { hostid, hostName }
   * filtered to only enabled hosts (status '0').
   * Triggerids with no enabled host are omitted from the map.
   */
  async getTriggerEnabledHosts(triggerids: string[]): Promise<Map<string, { hostid: string; hostName: string }>> {
    if (triggerids.length === 0) return new Map();
    const triggers = await this.rpc<Array<{
      triggerid: string;
      hosts: Array<{ hostid: string; name: string; status: string }>;
    }>>('trigger.get', {
      output: ['triggerid'],
      triggerids,
      selectHosts: ['hostid', 'name', 'status'],
    });

    const map = new Map<string, { hostid: string; hostName: string }>();
    for (const t of triggers) {
      const enabled = (t.hosts ?? []).find(h => h.status === '0');
      if (enabled) map.set(t.triggerid, { hostid: enabled.hostid, hostName: enabled.name });
    }
    return map;
  }

  /**
   * Create or update a Zabbix host. Idempotent — looks up by host name first.
   * Returns the hostid and whether the host was created or updated.
   */
  async upsertHost(params: ZabbixHostCreateParams): Promise<{
    action: 'created' | 'updated';
    hostid: string;
  }> {
    const existing = await this.findHostByName(params.host, params.name);

    if (!existing) {
      const result = await this.rpc<{ hostids: string[] }>('host.create', params);
      return { action: 'created', hostid: result.hostids[0] };
    }

    const updateParams: ZabbixHostUpdateParams = {
      hostid: existing.hostid,
      name: params.name,
      description: params.description,
      // Do not pass interfaces on update — Zabbix rejects changes when items
      // are already linked to the existing interface.
      groups: params.groups,
      templates: params.templates,
      macros: params.macros,
      tags: params.tags,
    };

    await this.rpc<{ hostids: string[] }>('host.update', updateParams);
    return { action: 'updated', hostid: existing.hostid };
  }

  /**
   * Find hosts by DNS name or IP address across all interfaces.
   * Used to correlate Datto RMM ping targets with Zabbix hosts.
   */
  async findHostsByInterface(dnsOrIp: string): Promise<Array<{ hostid: string; name: string }>> {
    return this.rpc<Array<{ hostid: string; name: string }>>('host.get', {
      output: ['hostid', 'name'],
      filter: { ip: [dnsOrIp], dns: [dnsOrIp] },
      searchByAny: true,
    });
  }

  /**
   * Get open problems for a host.
   */
  async getOpenProblemsForHost(hostid: string): Promise<ZabbixProblem[]> {
    return this.rpc<ZabbixProblem[]>('problem.get', {
      output: 'extend',
      hostids: [hostid],
      recent: true,
      selectAcknowledges: 'count',
      selectSuppressionData: 'extend',
    });
  }

  /**
   * Suppress a Zabbix problem event until a given timestamp.
   * action=16 = suppress, action=4 = acknowledge with message.
   * We combine both (action=20) to add a note and suppress.
   */
  async suppressProblem(eventid: string, _suppressUntil: Date, message: string): Promise<void> {
    await this.rpc<{ eventids: string[] }>('event.acknowledge', {
      eventids: [eventid],
      action: 20,  // 4 (add message) + 16 (suppress)
      message,
    });
  }
}
