/**
 * IT Glue API Client
 * Auth: x-api-key header
 * Base: https://api.itglue.com
 * Format: application/vnd.api+json (JSON:API)
 */

export interface ITGlueConfig {
  apiKey: string;
  baseUrl?: string;
}

export interface ITGlueOrganization {
  id: string;
  name: string;
  shortName: string | null;
  organizationTypeId: number | null;
  organizationTypeName: string | null;
  organizationStatusId: number | null;
  organizationStatusName: string | null;
  psaIntegration: string | null;
  syncActive: boolean;
  primary: boolean;
  createdAt: string;
  updatedAt: string;
}

export interface ITGlueFlexibleAsset {
  id: string;
  organizationId: number;
  organizationName: string;
  flexibleAssetTypeId: number;
  flexibleAssetTypeName: string;
  name: string;
  traits: Record<string, any>;
  createdAt: string;
  updatedAt: string;
}

export interface ITGlueConfiguration {
  id: string;
  organizationId: number;
  organizationName: string;
  name: string;
  hostname: string | null;
  primaryIp: string | null;
  macAddress: string | null;
  serialNumber: string | null;
  assetTag: string | null;
  configurationTypeId: number | null;
  configurationTypeName: string | null;
  configurationStatusId: number | null;
  configurationStatusName: string | null;
  manufacturerId: number | null;
  manufacturerName: string | null;
  modelId: number | null;
  modelName: string | null;
  operatingSystemId: number | null;
  operatingSystemName: string | null;
  notes: string | null;
  purchasedAt: string | null;
  createdAt: string;
  updatedAt: string;
}

export interface ITGluePassword {
  id: string;
  organizationId: number;
  organizationName: string;
  name: string;
  username: string | null;
  password: string | null;
  url: string | null;
  notes: string | null;
  passwordCategoryId: number | null;
  passwordCategoryName: string | null;
  createdAt: string;
  updatedAt: string;
}

export interface ITGlueContact {
  id: string;
  organizationId: number;
  organizationName: string;
  firstName: string | null;
  lastName: string | null;
  title: string | null;
  contactTypeId: number | null;
  contactTypeName: string | null;
  emails: { value: string; primary: boolean; labelName: string }[];
  phones: { value: string; extension: string | null; primary: boolean; labelName: string }[];
  notes: string | null;
  createdAt: string;
  updatedAt: string;
}

export interface ITGlueFlexibleAssetType {
  id: string;
  name: string;
  description: string | null;
  icon: string | null;
  enabled: boolean;
  createdAt: string;
  updatedAt: string;
}

export interface ITGluePaginatedResponse<T> {
  data: T[];
  meta: {
    currentPage: number;
    nextPage: number | null;
    prevPage: number | null;
    totalPages: number;
    totalCount: number;
  };
}

export class ITGlueClient {
  private readonly apiKey: string;
  private readonly baseUrl: string;
  private readonly DEFAULT_PAGE_SIZE = 50;

  constructor(config: ITGlueConfig) {
    this.apiKey = config.apiKey;
    this.baseUrl = config.baseUrl || 'https://api.itglue.com';
  }

  private get headers(): Record<string, string> {
    return {
      'x-api-key': this.apiKey,
      'Content-Type': 'application/vnd.api+json',
    };
  }

  private async request<T>(path: string, params: Record<string, string | number> = {}): Promise<T> {
    const url = new URL(`${this.baseUrl}${path}`);
    for (const [k, v] of Object.entries(params)) {
      url.searchParams.set(k, String(v));
    }

    const res = await fetch(url.toString(), { headers: this.headers });
    if (!res.ok) {
      const body = await res.text().catch(() => '');
      throw new Error(`IT Glue API ${res.status} ${res.statusText}: ${body.slice(0, 200)}`);
    }
    return res.json();
  }

  /**
   * Internal PATCH helper. Used by the audit feature to update flexible assets.
   * Body should be a JSON:API resource object (e.g. `{ data: { type, attributes } }`).
   * Returns the parsed JSON response (typically `{ data: {...} }`).
   */
  private async patch<T = unknown>(path: string, body: unknown): Promise<T> {
    const res = await fetch(`${this.baseUrl}${path}`, {
      method: 'PATCH',
      headers: this.headers,
      body: JSON.stringify(body),
    });
    if (!res.ok) {
      const text = await res.text().catch(() => '');
      throw new Error(
        `IT Glue PATCH ${path} → ${res.status} ${res.statusText}: ${text.slice(0, 500)}`
      );
    }
    return res.json() as Promise<T>;
  }

  private async fetchAllPages<T>(
    path: string,
    params: Record<string, string | number> = {},
    mapper: (item: any) => T
  ): Promise<T[]> {
    const results: T[] = [];
    let page = 1;
    let totalPages = 1;

    do {
      const data: any = await this.request(path, {
        ...params,
        'page[size]': this.DEFAULT_PAGE_SIZE,
        'page[number]': page,
      });
      results.push(...(data.data || []).map(mapper));
      totalPages = data.meta?.['total-pages'] ?? 1;
      page++;
    } while (page <= totalPages);

    return results;
  }

  /** Returns raw JSON:API data array for a single page (used by sync service) */
  async getRaw(path: string, params: Record<string, string | number> = {}): Promise<any[]> {
    const data: any = await this.request(path, params);
    return data.data || [];
  }

  /**
   * Returns the raw JSON:API resource for a single-resource endpoint
   * (`{ data: {...} }`). Used for per-record refresh after writes so callers
   * can read the un-mapped attributes (created-at, updated-at, etc.) for a
   * faithful upsert.
   */
  async getRawSingle(
    path: string,
    params: Record<string, string | number> = {}
  ): Promise<{ id: string; type: string; attributes: Record<string, unknown> } | null> {
    const data: any = await this.request(path, params);
    return data.data ?? null;
  }

  /** Returns all raw JSON:API data items across all pages (used by sync service) */
  async getRawAllPages(path: string, params: Record<string, string | number> = {}): Promise<any[]> {
    const results: any[] = [];
    let page = 1;
    let totalPages = 1;

    do {
      const data: any = await this.request(path, {
        ...params,
        'page[size]': this.DEFAULT_PAGE_SIZE,
        'page[number]': page,
      });
      results.push(...(data.data || []));
      totalPages = data.meta?.['total-pages'] ?? 1;
      page++;
    } while (page <= totalPages);

    return results;
  }

  // ─── Organizations ────────────────────────────────────────────────────────

  private mapOrg(item: any): ITGlueOrganization {
    const a = item.attributes;
    return {
      id: item.id,
      name: a['name'],
      shortName: a['short-name'] ?? null,
      organizationTypeId: a['organization-type-id'] ?? null,
      organizationTypeName: a['organization-type-name'] ?? null,
      organizationStatusId: a['organization-status-id'] ?? null,
      organizationStatusName: a['organization-status-name'] ?? null,
      psaIntegration: a['psa-integration'] ?? null,
      syncActive: a['sync-active'] ?? false,
      primary: a['primary'] ?? false,
      createdAt: a['created-at'],
      updatedAt: a['updated-at'],
    };
  }

  async getOrganizations(filter?: { name?: string; organizationTypeId?: number }): Promise<ITGlueOrganization[]> {
    const params: Record<string, string | number> = {};
    if (filter?.name) params['filter[name]'] = filter.name;
    if (filter?.organizationTypeId) params['filter[organization-type-id]'] = filter.organizationTypeId;
    return this.fetchAllPages('/organizations', params, this.mapOrg);
  }

  async getOrganization(id: string | number): Promise<ITGlueOrganization> {
    const data: any = await this.request(`/organizations/${id}`);
    return this.mapOrg(data.data);
  }

  async findOrganizationByName(name: string): Promise<ITGlueOrganization | null> {
    const data: any = await this.request('/organizations', {
      'filter[name]': name,
      'page[size]': 5,
    });
    if (!data.data?.length) return null;
    return this.mapOrg(data.data[0]);
  }

  // ─── Flexible Assets ──────────────────────────────────────────────────────

  private mapFlexibleAsset(item: any): ITGlueFlexibleAsset {
    const a = item.attributes;
    return {
      id: item.id,
      organizationId: a['organization-id'],
      organizationName: a['organization-name'],
      flexibleAssetTypeId: a['flexible-asset-type-id'],
      flexibleAssetTypeName: a['flexible-asset-type-name'],
      name: a['name'],
      traits: a['traits'] ?? {},
      createdAt: a['created-at'],
      updatedAt: a['updated-at'],
    };
  }

  async getFlexibleAssets(params: {
    organizationId?: number | string;
    flexibleAssetTypeId?: number | string;
    filter?: Record<string, string>;
  }): Promise<ITGlueFlexibleAsset[]> {
    const p: Record<string, string | number> = {};
    if (params.organizationId) p['filter[organization-id]'] = params.organizationId;
    if (params.flexibleAssetTypeId) p['filter[flexible-asset-type-id]'] = params.flexibleAssetTypeId;
    if (params.filter) {
      for (const [k, v] of Object.entries(params.filter)) {
        p[`filter[${k}]`] = v;
      }
    }
    return this.fetchAllPages('/flexible_assets', p, this.mapFlexibleAsset);
  }

  async getFlexibleAsset(id: string | number): Promise<ITGlueFlexibleAsset> {
    const data: any = await this.request(`/flexible_assets/${id}`);
    return this.mapFlexibleAsset(data.data);
  }

  /**
   * Update a flexible asset's traits. Sends PATCH /flexible_assets/:id with a
   * JSON:API body. `traits` is the merged trait map (IT Glue replaces the
   * trait set, so callers must include unchanged traits to preserve them; the
   * audit pipeline always reads then merges).
   *
   * Returns the updated asset as IT Glue returns it.
   */
  async updateFlexibleAsset(
    id: string | number,
    traits: Record<string, unknown>
  ): Promise<ITGlueFlexibleAsset> {
    const body = {
      data: {
        type: 'flexible_assets',
        id: String(id),
        attributes: { traits },
      },
    };
    const res = await this.patch<{ data: any }>(`/flexible_assets/${id}`, body);
    return this.mapFlexibleAsset(res.data);
  }

  /**
   * Re-fetch a single flexible asset from IT Glue. Thin wrapper around
   * getFlexibleAsset; exists so callers naming "refresh" intent stays clear
   * separate from "read once".
   */
  async refreshFlexibleAsset(id: string | number): Promise<ITGlueFlexibleAsset> {
    return this.getFlexibleAsset(id);
  }

  /**
   * Update a configuration's editable attributes. Sends PATCH /configurations/:id
   * with a JSON:API body. Configurations have a flat attribute set (no traits
   * blob), so callers pass the partial map of fields to change — IT Glue
   * merges into the existing record.
   */
  async updateConfiguration(
    id: string | number,
    attributes: Record<string, unknown>
  ): Promise<ITGlueConfiguration> {
    const body = {
      data: {
        type: 'configurations',
        id: String(id),
        attributes,
      },
    };
    const res = await this.patch<{ data: any }>(`/configurations/${id}`, body);
    return this.mapConfiguration(res.data);
  }

  async refreshConfiguration(id: string | number): Promise<ITGlueConfiguration> {
    return this.getConfiguration(id);
  }

  async getFlexibleAssetTypes(): Promise<ITGlueFlexibleAssetType[]> {
    return this.fetchAllPages('/flexible_asset_types', {}, (item: any) => ({
      id: item.id,
      name: item.attributes['name'],
      description: item.attributes['description'] ?? null,
      icon: item.attributes['icon'] ?? null,
      enabled: item.attributes['enabled'] ?? true,
      createdAt: item.attributes['created-at'],
      updatedAt: item.attributes['updated-at'],
    }));
  }

  /**
   * Cached, per-instance fetch of enabled flexible asset type ids. IT Glue's
   * /flexible_assets endpoint requires a flexibleAssetTypeId filter (otherwise
   * 422). This caches the type list so callers don't pay the lookup on every
   * call. Cache lives for the life of the process — types rarely change.
   */
  private flexibleAssetTypesCache: Promise<ITGlueFlexibleAssetType[]> | null = null;
  private async cachedFlexibleAssetTypes(): Promise<ITGlueFlexibleAssetType[]> {
    if (!this.flexibleAssetTypesCache) {
      this.flexibleAssetTypesCache = this.getFlexibleAssetTypes().catch((err) => {
        // Re-throw next call so a transient failure isn't sticky.
        this.flexibleAssetTypesCache = null;
        throw err;
      });
    }
    return this.flexibleAssetTypesCache;
  }

  /**
   * Get every flexible asset for a given organization across all enabled types.
   * IT Glue's /flexible_assets endpoint requires a per-type filter (the
   * `getFlexibleAssets` raw call returns 422 otherwise), so this helper
   * enumerates types and fans out per-type requests in parallel. Per-type
   * failures are tolerated so a single bad type doesn't poison the whole org.
   */
  async getFlexibleAssetsForOrganization(
    organizationId: number | string
  ): Promise<ITGlueFlexibleAsset[]> {
    const types = await this.cachedFlexibleAssetTypes();
    const enabled = types.filter((t) => t.enabled);
    const results = await Promise.allSettled(
      enabled.map((t) =>
        this.getFlexibleAssets({
          organizationId,
          flexibleAssetTypeId: t.id,
        })
      )
    );
    const out: ITGlueFlexibleAsset[] = [];
    for (const r of results) {
      if (r.status === 'fulfilled') out.push(...r.value);
      // Tolerate per-type failures — common for permission-restricted types.
    }
    return out;
  }

  // ─── Configurations ───────────────────────────────────────────────────────

  private mapConfiguration(item: any): ITGlueConfiguration {
    const a = item.attributes;
    return {
      id: item.id,
      organizationId: a['organization-id'],
      organizationName: a['organization-name'],
      name: a['name'],
      hostname: a['hostname'] ?? null,
      primaryIp: a['primary-ip'] ?? null,
      macAddress: a['mac-address'] ?? null,
      serialNumber: a['serial-number'] ?? null,
      assetTag: a['asset-tag'] ?? null,
      configurationTypeId: a['configuration-type-id'] ?? null,
      configurationTypeName: a['configuration-type-name'] ?? null,
      configurationStatusId: a['configuration-status-id'] ?? null,
      configurationStatusName: a['configuration-status-name'] ?? null,
      manufacturerId: a['manufacturer-id'] ?? null,
      manufacturerName: a['manufacturer-name'] ?? null,
      modelId: a['model-id'] ?? null,
      modelName: a['model-name'] ?? null,
      operatingSystemId: a['operating-system-id'] ?? null,
      operatingSystemName: a['operating-system-name'] ?? null,
      notes: a['notes'] ?? null,
      purchasedAt: a['purchased-at'] ?? null,
      createdAt: a['created-at'],
      updatedAt: a['updated-at'],
    };
  }

  async getConfigurations(params: {
    organizationId?: number | string;
    name?: string;
    hostname?: string;
    serialNumber?: string;
  } = {}): Promise<ITGlueConfiguration[]> {
    const p: Record<string, string | number> = {};
    if (params.organizationId) p['filter[organization-id]'] = params.organizationId;
    if (params.name) p['filter[name]'] = params.name;
    if (params.hostname) p['filter[hostname]'] = params.hostname;
    if (params.serialNumber) p['filter[serial-number]'] = params.serialNumber;
    return this.fetchAllPages('/configurations', p, this.mapConfiguration);
  }

  async getConfiguration(id: string | number): Promise<ITGlueConfiguration> {
    const data: any = await this.request(`/configurations/${id}`);
    return this.mapConfiguration(data.data);
  }

  // ─── Passwords ────────────────────────────────────────────────────────────

  private mapPassword(item: any): ITGluePassword {
    const a = item.attributes;
    return {
      id: item.id,
      organizationId: a['organization-id'],
      organizationName: a['organization-name'],
      name: a['name'],
      username: a['username'] ?? null,
      password: a['password'] ?? null,
      url: a['url'] ?? null,
      notes: a['notes'] ?? null,
      passwordCategoryId: a['password-category-id'] ?? null,
      passwordCategoryName: a['password-category-name'] ?? null,
      createdAt: a['created-at'],
      updatedAt: a['updated-at'],
    };
  }

  async getPasswords(params: {
    organizationId?: number | string;
    name?: string;
  } = {}): Promise<ITGluePassword[]> {
    const p: Record<string, string | number> = {};
    if (params.organizationId) p['filter[organization-id]'] = params.organizationId;
    if (params.name) p['filter[name]'] = params.name;
    return this.fetchAllPages('/passwords', p, this.mapPassword);
  }

  // ─── Contacts ─────────────────────────────────────────────────────────────

  private mapContact(item: any): ITGlueContact {
    const a = item.attributes;
    return {
      id: item.id,
      organizationId: a['organization-id'],
      organizationName: a['organization-name'],
      firstName: a['first-name'] ?? null,
      lastName: a['last-name'] ?? null,
      title: a['title'] ?? null,
      contactTypeId: a['contact-type-id'] ?? null,
      contactTypeName: a['contact-type-name'] ?? null,
      emails: (a['contact-emails'] ?? []).map((e: any) => ({
        value: e.value,
        primary: e.primary,
        labelName: e['label-name'],
      })),
      phones: (a['contact-phones'] ?? []).map((p: any) => ({
        value: p.value,
        extension: p.extension ?? null,
        primary: p.primary,
        labelName: p['label-name'],
      })),
      notes: a['notes'] ?? null,
      createdAt: a['created-at'],
      updatedAt: a['updated-at'],
    };
  }

  async getContacts(params: {
    organizationId?: number | string;
    name?: string;
  } = {}): Promise<ITGlueContact[]> {
    const p: Record<string, string | number> = {};
    if (params.organizationId) p['filter[organization-id]'] = params.organizationId;
    if (params.name) p['filter[name]'] = params.name;
    return this.fetchAllPages('/contacts', p, this.mapContact);
  }

  // ─── Utility ──────────────────────────────────────────────────────────────

  async testConnection(): Promise<{ ok: boolean; organizationCount: number; accountName: string }> {
    const data: any = await this.request('/organizations', { 'page[size]': 1 });
    return {
      ok: true,
      organizationCount: data.meta?.['total-count'] ?? 0,
      accountName: data.data?.[0]?.attributes?.name ?? 'Unknown',
    };
  }
}

// Singleton
let _client: ITGlueClient | null = null;
export function getITGlueClient(): ITGlueClient {
  if (!_client) {
    const apiKey = process.env.ITGLUE_API_KEY;
    if (!apiKey) throw new Error('ITGLUE_API_KEY environment variable is not set');
    _client = new ITGlueClient({ apiKey });
  }
  return _client;
}

export function isITGlueConfigured(): boolean {
  return !!process.env.ITGLUE_API_KEY;
}
