import { env } from '../config/env';
import { logger } from '../utils/logger';

// --- Types ---

export interface PangolinSite {
  siteId: string;
  name: string;
  orgId: string;
  niceId: string;
  pubKey?: string;
  subnet?: string;
  megabytesIn?: number;
  megabytesOut?: number;
  lastSeen?: string;
  online?: boolean;
  type?: string;
  address?: string;
}

export interface PangolinExitNode {
  exitNodeId: string;
  name: string;
  location?: string;
  region?: string;
  online: boolean;
  capacity?: number;
  latency?: number;
}

export interface PangolinResource {
  resourceId: string;
  name: string;
  subdomain?: string;
  fullDomain?: string;
  ssl?: boolean;
  blockAccess?: boolean;
  active?: boolean;
  proxyPort?: number;
  protocol?: string;
  domainBindings?: string[];
  http?: boolean;
  // Target info (returned by list endpoints)
  targets?: PangolinTarget[];
}

export interface PangolinTarget {
  targetId: string;
  resourceId: string;
  siteId: string;
  ip: string;
  port: number;
  method: string;
  enabled?: boolean;
}

export interface PangolinNewt {
  newtId: string;
  secret: string;
  siteId: string;
}

export interface PangolinSiteDefaults {
  newtId: string;
  newtSecret: string;
  address: string;
}

export interface CreateSitePayload {
  name: string;
  type?: string;
  subnet?: string;
  exitNodeId?: string;
  // Newt credentials from pickSiteDefaults
  newtId?: string;
  secret?: string;
  address?: string;
}

// HTTP Resource (public proxy) - Correct Pangolin API schema
// Uses endpoint: PUT /org/{orgId}/resource (NOT /site-resource)
export interface CreateHttpResourcePayload {
  name: string;
  domainId: string;
  subdomain?: string;  // Subdomain only (e.g., "app"); omit entirely for root domain
  http: true;         // REQUIRED: marks as HTTP proxy resource
  protocol: 'tcp';    // REQUIRED for HTTP resources
  // Note: ssl and enabled are NOT valid creation fields — set via updateResource()
}

export interface CreateTargetPayload {
  siteId: string | number;  // REQUIRED: which site routes this traffic (Pangolin expects numeric)
  ip: string;               // Target hostname/IP (e.g., "nginx")
  port: number;             // Target port (e.g., 80)
  method: 'http' | 'https';
  enabled?: boolean;
}

export interface PangolinDomain {
  domainId: string;
  baseDomain: string;
  verified: boolean;
  type?: string;
  failed?: boolean;
  configManaged?: boolean;
}

export interface UpdateResourcePayload {
  name?: string;
  subdomain?: string;
  fullDomain?: string;
  ssl?: boolean;
  sso?: boolean;
  active?: boolean;
  blockAccess?: boolean;
  proxyPort?: number;
  protocol?: string;
  domainBindings?: string[];
}

export interface PangolinCertificate {
  certId: string;
  domainId: string;
  domain: string;
  status: 'PENDING' | 'ACTIVE' | 'EXPIRED' | 'FAILED';
  issuedAt?: string;
  expiresAt?: string;
  autoRenew?: boolean;
  issuer?: string;
}

export interface UpdateCertificatePayload {
  autoRenew?: boolean;
}

export interface PangolinConnectedClient {
  clientId: string;
  resourceId: string;
  ipAddress: string;
  connectedAt: string;
  lastSeen: string;
  bytesIn: number;
  bytesOut: number;
  online: boolean;
}

// --- Client ---

class PangolinClient {
  private get baseUrl(): string {
    return env.PANGOLIN_API_URL;
  }

  private get apiKey(): string {
    return env.PANGOLIN_API_KEY;
  }

  private get orgId(): string {
    return env.PANGOLIN_ORG_ID;
  }

  get configured(): boolean {
    return !!(this.baseUrl && this.apiKey && this.orgId);
  }

  private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
    if (!this.baseUrl || !this.apiKey) {
      throw new Error('Pangolin API not configured. Set PANGOLIN_API_URL and PANGOLIN_API_KEY.');
    }

    const url = `${this.baseUrl}${path}`;
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 15000);

    try {
      logger.debug(`Pangolin ${method} ${path}${body ? ` body=${JSON.stringify(body)}` : ''}`);

      const res = await fetch(url, {
        method,
        headers: {
          'Authorization': `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json',
        },
        body: body ? JSON.stringify(body) : undefined,
        signal: controller.signal,
      });

      if (!res.ok) {
        const text = await res.text().catch(() => '');
        logger.error(`Pangolin API ${method} ${path} returned ${res.status}: ${text}`);
        throw new Error(`Pangolin API ${method} ${path} returned ${res.status}: ${text}`);
      }

      const contentType = res.headers.get('content-type') || '';
      if (contentType.includes('application/json')) {
        const json = await res.json();
        // Pangolin wraps responses in { data: {...}, success, status }
        // Unwrap if present
        return this.unwrapResponse<T>(json);
      }
      return {} as T;
    } finally {
      clearTimeout(timeout);
    }
  }

  /**
   * Unwrap Pangolin's response envelope: { data: {...}, success, status }
   * Returns the inner data object, or the raw response if not wrapped.
   */
  private unwrapResponse<T>(json: unknown): T {
    if (json && typeof json === 'object' && !Array.isArray(json)) {
      const obj = json as Record<string, unknown>;
      // If it has a 'data' key and 'success' key, it's a wrapped response
      if ('data' in obj && 'success' in obj) {
        logger.debug('Unwrapped Pangolin response envelope');
        return obj.data as T;
      }
    }
    return json as T;
  }

  async healthCheck(): Promise<boolean> {
    try {
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 5000);
      try {
        const res = await fetch(`${this.baseUrl}/`, {
          headers: { 'Authorization': `Bearer ${this.apiKey}` },
          signal: controller.signal,
        });
        return res.ok;
      } finally {
        clearTimeout(timeout);
      }
    } catch (err) {
      logger.warn('Pangolin health check failed:', err instanceof Error ? err.message : err);
      return false;
    }
  }

  // --- Site Defaults ---

  /**
   * Get pre-generated Newt credentials for a new site.
   * Must be called BEFORE createSite() and passed into it.
   * Endpoint: GET /org/{orgId}/pick-site-defaults
   */
  async pickSiteDefaults(): Promise<PangolinSiteDefaults> {
    const res = await this.request<unknown>('GET', `/org/${this.orgId}/pick-site-defaults`);
    const obj = res as Record<string, unknown>;

    // Response format: { newtId, newtSecret, clientAddress, subnet, ... }
    // Note: `address` is the exit node address, `clientAddress` is the site address
    const newtId = obj.newtId as string || '';
    const newtSecret = obj.newtSecret as string || obj.secret as string || '';
    const address = obj.clientAddress as string || obj.address as string || '';

    if (!newtId || !newtSecret) {
      logger.warn('pickSiteDefaults response missing newtId/newtSecret:', JSON.stringify(res));
      throw new Error('Pangolin did not return Newt credentials from pick-site-defaults');
    }

    logger.info(`pickSiteDefaults: newtId=${newtId}, address=${address}`);
    return { newtId, newtSecret, address };
  }

  // --- Sites ---

  async listSites(): Promise<PangolinSite[]> {
    const res = await this.request<unknown>('GET', `/org/${this.orgId}/sites`);
    return this.extractArray(res, 'sites', 'listSites');
  }

  async listExitNodes(): Promise<PangolinExitNode[]> {
    try {
      const res = await this.request<unknown>('GET', `/org/${this.orgId}/exit-nodes`);
      return this.extractArray(res, 'exitNodes', 'listExitNodes');
    } catch (err) {
      if (err instanceof Error && err.message.includes('404')) {
        logger.info('Pangolin exit-nodes endpoint not available (self-hosted mode)');
        return [];
      }
      logger.warn('Failed to fetch exit nodes:', err);
      return [];
    }
  }

  async getSite(siteId: string): Promise<PangolinSite> {
    return this.request<PangolinSite>('GET', `/site/${siteId}`);
  }

  async createSite(data: CreateSitePayload): Promise<PangolinSite & { newt?: PangolinNewt }> {
    return this.request<PangolinSite & { newt?: PangolinNewt }>(
      'PUT',
      `/org/${this.orgId}/site`,
      data,
    );
  }

  async deleteSite(siteId: string): Promise<void> {
    await this.request<void>('DELETE', `/site/${siteId}`);
  }

  // --- HTTP Resources (public proxy) ---

  /**
   * List HTTP proxy resources (public resources).
   * Endpoint: GET /org/{orgId}/resources (NOT /site-resources)
   */
  async listResources(): Promise<PangolinResource[]> {
    const res = await this.request<unknown>('GET', `/org/${this.orgId}/resources`);
    return this.extractArray(res, 'resources', 'listResources');
  }

  async getResource(resourceId: string): Promise<PangolinResource> {
    return this.request<PangolinResource>('GET', `/resource/${resourceId}`);
  }

  /**
   * Create an HTTP proxy resource (public resource).
   * Endpoint: PUT /org/{orgId}/resource (NOT /site-resource)
   */
  async createResource(data: CreateHttpResourcePayload): Promise<PangolinResource> {
    logger.info(`createResource: ${data.name} (subdomain: ${data.subdomain || '(root)'})`);
    return this.request<PangolinResource>(
      'PUT',
      `/org/${this.orgId}/resource`,
      data,
    );
  }

  async updateResource(resourceId: string, data: UpdateResourcePayload): Promise<PangolinResource> {
    return this.request<PangolinResource>('POST', `/resource/${resourceId}`, data);
  }

  async deleteResource(resourceId: string): Promise<void> {
    await this.request<void>('DELETE', `/resource/${resourceId}`);
  }

  // --- Targets ---

  /**
   * Create a target for a resource (routes traffic to a backend).
   * Endpoint: PUT /resource/{resourceId}/target (PUT, not POST)
   */
  async createTarget(resourceId: string, data: CreateTargetPayload): Promise<PangolinTarget> {
    logger.info(`createTarget: resource=${resourceId}, ip=${data.ip}:${data.port}, method=${data.method}`);
    // Pangolin expects siteId as a number, not a string
    const payload = { ...data, siteId: Number(data.siteId) };
    return this.request<PangolinTarget>(
      'PUT',
      `/resource/${resourceId}/target`,
      payload,
    );
  }

  /**
   * List targets for a resource.
   * Endpoint: GET /resource/{resourceId}/targets (plural — NOT /target)
   */
  async listTargets(resourceId: string): Promise<PangolinTarget[]> {
    const res = await this.request<unknown>('GET', `/resource/${resourceId}/targets`);
    return this.extractArray(res, 'targets', 'listTargets');
  }

  /**
   * Delete a target by ID.
   * Endpoint: DELETE /target/{targetId} (NOT nested under /resource/)
   */
  async deleteTarget(targetId: string): Promise<void> {
    await this.request<void>('DELETE', `/target/${targetId}`);
  }

  // --- Domains ---

  async listDomains(): Promise<PangolinDomain[]> {
    const res = await this.request<unknown>('GET', `/org/${this.orgId}/domains`);
    return this.extractArray(res, 'domains', 'listDomains');
  }

  // --- Certificates ---

  async getCertificate(domainId: string, domain: string): Promise<PangolinCertificate> {
    return this.request<PangolinCertificate>(
      'GET',
      `/org/${this.orgId}/certificate/${domainId}/${domain}`,
    );
  }

  async updateCertificate(certId: string, data: UpdateCertificatePayload): Promise<PangolinCertificate> {
    return this.request<PangolinCertificate>('POST', `/certificate/${certId}`, data);
  }

  // --- Clients ---

  async listClients(resourceId: string): Promise<PangolinConnectedClient[]> {
    const res = await this.request<unknown>('GET', `/resource/${resourceId}/clients`);
    return this.extractArray(res, 'clients', 'listClients');
  }

  // --- Helpers ---

  /**
   * Extract an array from a Pangolin response that may be:
   * - A direct array
   * - An object with the array under a named key (e.g., { sites: [...] })
   * - An object with { data: [...] }
   */
  private extractArray<T>(res: unknown, key: string, context: string): T[] {
    if (Array.isArray(res)) {
      return res as T[];
    }

    if (res && typeof res === 'object') {
      const obj = res as Record<string, unknown>;

      // Check named key (e.g., "sites", "resources", "domains")
      if (Array.isArray(obj[key])) {
        return obj[key] as T[];
      }

      // Check nested data.{key}
      if (obj.data && typeof obj.data === 'object') {
        const dataObj = obj.data as Record<string, unknown>;
        if (Array.isArray(dataObj[key])) {
          return dataObj[key] as T[];
        }
      }

      // Fallback: { data: [...] }
      if (Array.isArray(obj.data)) {
        return obj.data as T[];
      }
    }

    logger.warn(`${context}: could not extract array from response, returning empty`);
    return [];
  }
}

export const pangolinClient = new PangolinClient();
