import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import axios from 'axios';
import {
  BaseConnector,
  ConnectionTestResult,
} from '../base-connector';
import {
  IntegrationAuthError,
  IntegrationConfigError,
} from '../../errors';
import { CredentialsService } from '../../credentials/credentials.service';
import {
  PlentyoneAuthConfig,
  PlentyoneTokenInfo,
  PlentyoneTokenResponse,
  PlentyoneOrder,
  PlentyoneOrdersQuery,
  PlentyonePaginatedResponse,
  PlentyoneStockItem,
  PlentyoneStockQuery,
  PlentyoneOrderStats,
  PlentyoneStatsQuery,
  PlentyoneRevenueStats,
} from './plentyone.types';

/**
 * PlentyONE API Connector
 *
 * Provides integration with PlentyONE e-commerce platform.
 * Handles OAuth2 authentication, token management, and API calls.
 * Credentials are loaded lazily from the DB via CredentialsService.
 *
 * @see https://developers.plentymarkets.com/
 */
@Injectable()
export class PlentyoneConnector extends BaseConnector {
  protected readonly name = 'PlentyONE';
  private authConfig: PlentyoneAuthConfig = { baseUrl: '', clientId: '', clientSecret: '' };
  private tokenInfo: PlentyoneTokenInfo | null = null;
  private tokenRefreshPromise: Promise<void> | null = null;
  private initialized = false;
  private configuredState = false;

  constructor(private readonly credentialsService: CredentialsService) {
    super();
  }

  /**
   * Load credentials from DB and configure the connector.
   * Idempotent - returns immediately if already initialized.
   */
  async ensureInitialized(): Promise<void> {
    if (this.initialized) return;

    const result = await this.credentialsService.findActiveByType('PLENTYONE');

    if (!result) {
      this.configuredState = false;
      this.initialized = true;
      return;
    }

    const { baseUrl, clientId, clientSecret } = result.credentials;

    this.authConfig = {
      baseUrl: baseUrl || '',
      clientId: clientId || '',
      clientSecret: clientSecret || '',
    };

    if (this.authConfig.baseUrl) {
      this.reconfigure({
        baseUrl: `${this.authConfig.baseUrl}/rest`,
        timeout: 60000,
        maxRetries: 3,
      });
    }

    this.configuredState = !!(
      this.authConfig.baseUrl &&
      this.authConfig.clientId &&
      this.authConfig.clientSecret
    );
    this.initialized = true;
  }

  /**
   * Reload credentials from DB (called when credentials change)
   */
  async reload(): Promise<void> {
    this.initialized = false;
    this.tokenInfo = null;
    this.tokenRefreshPromise = null;
    await this.ensureInitialized();
  }

  /**
   * Handle credential change events
   */
  @OnEvent('credentials.changed')
  async onCredentialsChanged(payload: { type: string }): Promise<void> {
    if (payload.type === 'PLENTYONE') {
      this.logger.log('PlentyONE credentials changed, reloading');
      await this.reload();
    }
  }

  /**
   * Check if the connector is properly configured (last known state)
   */
  isConfigured(): boolean {
    return this.configuredState;
  }

  /**
   * Get missing configuration items
   */
  getMissingConfig(): string[] {
    const missing: string[] = [];
    if (!this.authConfig.baseUrl) missing.push('baseUrl');
    if (!this.authConfig.clientId) missing.push('clientId');
    if (!this.authConfig.clientSecret) missing.push('clientSecret');
    return missing;
  }

  /**
   * Test the connection to PlentyONE
   */
  async testConnection(): Promise<ConnectionTestResult> {
    await this.ensureInitialized();

    if (!this.isConfigured()) {
      return {
        success: false,
        message: `Missing configuration: ${this.getMissingConfig().join(', ')}`,
      };
    }

    const startTime = Date.now();

    try {
      await this.ensureAuthenticated();
      await this.get<{ version: string }>('/');

      return {
        success: true,
        message: 'Successfully connected to PlentyONE',
        latencyMs: Date.now() - startTime,
        details: {
          baseUrl: this.authConfig.baseUrl,
        },
      };
    } catch (error) {
      const message = error instanceof Error ? error.message : 'Unknown error';
      return {
        success: false,
        message: `Connection failed: ${message}`,
        latencyMs: Date.now() - startTime,
      };
    }
  }

  /**
   * Get authentication headers for requests
   */
  async getAuthHeaders(): Promise<Record<string, string>> {
    await this.ensureInitialized();

    if (!this.isConfigured()) {
      throw new IntegrationConfigError(this.name, this.getMissingConfig());
    }

    await this.ensureAuthenticated();

    return {
      Authorization: `Bearer ${this.tokenInfo!.accessToken}`,
    };
  }

  /**
   * Ensure we have a valid access token
   */
  private async ensureAuthenticated(): Promise<void> {
    if (this.isTokenValid()) {
      return;
    }

    if (this.tokenRefreshPromise) {
      await this.tokenRefreshPromise;
      return;
    }

    this.tokenRefreshPromise = this.refreshAccessToken();

    try {
      await this.tokenRefreshPromise;
    } finally {
      this.tokenRefreshPromise = null;
    }
  }

  /**
   * Check if the current token is still valid
   */
  private isTokenValid(): boolean {
    if (!this.tokenInfo) return false;

    const bufferMs = 5 * 60 * 1000;
    return this.tokenInfo.expiresAt.getTime() > Date.now() + bufferMs;
  }

  /**
   * Refresh the access token using client credentials
   */
  private async refreshAccessToken(): Promise<void> {
    this.logger.debug('Refreshing PlentyONE access token');

    try {
      const response = await axios.post<PlentyoneTokenResponse>(
        `${this.authConfig.baseUrl}/rest/oauth/access_token`,
        new URLSearchParams({
          grant_type: 'client_credentials',
          client_id: this.authConfig.clientId,
          client_secret: this.authConfig.clientSecret,
        }),
        {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
          },
          timeout: 30000,
        },
      );

      this.tokenInfo = {
        accessToken: response.data.access_token,
        expiresAt: new Date(Date.now() + response.data.expires_in * 1000),
        refreshToken: response.data.refresh_token,
      };

      this.logger.debug(
        `PlentyONE token refreshed, expires at ${this.tokenInfo.expiresAt.toISOString()}`,
      );
    } catch (error) {
      this.logger.error('Failed to refresh PlentyONE token', error);
      throw new IntegrationAuthError(
        this.name,
        'Failed to obtain access token',
        error instanceof Error ? error : undefined,
      );
    }
  }

  // ===========================================================================
  // Orders API
  // ===========================================================================

  /**
   * Get a list of orders
   */
  async getOrders(
    query?: PlentyoneOrdersQuery,
  ): Promise<PlentyonePaginatedResponse<PlentyoneOrder>> {
    await this.ensureInitialized();
    const params = this.buildQueryParams(query);
    return this.get<PlentyonePaginatedResponse<PlentyoneOrder>>(
      '/orders',
      { params },
    );
  }

  /**
   * Get a single order by ID
   */
  async getOrder(
    orderId: number,
    withRelations?: string[],
  ): Promise<PlentyoneOrder> {
    await this.ensureInitialized();
    const params: Record<string, string> = {};
    if (withRelations?.length) {
      params.with = withRelations.join(',');
    }
    return this.get<PlentyoneOrder>(`/orders/${orderId}`, { params });
  }

  /**
   * Search orders by external order ID
   */
  async searchOrdersByExternalId(
    externalOrderId: string,
  ): Promise<PlentyonePaginatedResponse<PlentyoneOrder>> {
    return this.getOrders({ externalOrderId });
  }

  /**
   * Get orders by status
   */
  async getOrdersByStatus(
    statusId: number,
    page = 1,
    itemsPerPage = 50,
  ): Promise<PlentyonePaginatedResponse<PlentyoneOrder>> {
    return this.getOrders({ statusId, page, itemsPerPage });
  }

  /**
   * Get orders within a date range
   */
  async getOrdersByDateRange(
    dateFrom: Date,
    dateTo: Date,
    page = 1,
    itemsPerPage = 50,
  ): Promise<PlentyonePaginatedResponse<PlentyoneOrder>> {
    return this.getOrders({
      createdAtFrom: dateFrom.toISOString(),
      createdAtTo: dateTo.toISOString(),
      page,
      itemsPerPage,
    });
  }

  // ===========================================================================
  // Stock API
  // ===========================================================================

  /**
   * Get stock levels
   */
  async getStock(
    query?: PlentyoneStockQuery,
  ): Promise<PlentyonePaginatedResponse<PlentyoneStockItem>> {
    await this.ensureInitialized();
    const params = this.buildQueryParams(query);
    return this.get<PlentyonePaginatedResponse<PlentyoneStockItem>>(
      '/stockmanagement/stock',
      { params },
    );
  }

  /**
   * Get stock for a specific variation
   */
  async getVariationStock(variationId: number): Promise<PlentyoneStockItem[]> {
    const response = await this.getStock({ variationId });
    return response.entries;
  }

  /**
   * Get stock for a specific warehouse
   */
  async getWarehouseStock(
    warehouseId: number,
    page = 1,
    itemsPerPage = 50,
  ): Promise<PlentyonePaginatedResponse<PlentyoneStockItem>> {
    return this.getStock({ warehouseId, page, itemsPerPage });
  }

  /**
   * Get low stock items (below reorder level)
   */
  async getLowStockItems(
    warehouseId?: number,
  ): Promise<PlentyoneStockItem[]> {
    const query: PlentyoneStockQuery = { itemsPerPage: 250 };
    if (warehouseId) {
      query.warehouseId = warehouseId;
    }

    const allItems: PlentyoneStockItem[] = [];
    let page = 1;
    let hasMore = true;

    while (hasMore) {
      const response = await this.getStock({ ...query, page });
      const lowStockItems = response.entries.filter(
        (item) => item.stockNet <= item.reorderDelta,
      );
      allItems.push(...lowStockItems);

      hasMore = !response.isLastPage;
      page++;

      if (page > 100) break;
    }

    return allItems;
  }

  // ===========================================================================
  // Statistics API
  // ===========================================================================

  /**
   * Get order statistics
   */
  async getOrderStats(query: PlentyoneStatsQuery): Promise<PlentyoneOrderStats> {
    const orders = await this.getAllOrdersInRange(
      new Date(query.dateFrom),
      new Date(query.dateTo),
      query.statusId,
    );

    let totalRevenue = 0;
    let totalRevenueNet = 0;
    const ordersByStatus: Record<number, number> = {};
    const ordersByReferrer: Record<number, number> = {};

    for (const order of orders) {
      if (order.amounts?.length) {
        const primaryAmount = order.amounts.find((a) => a.isSystemCurrency);
        if (primaryAmount) {
          totalRevenue += primaryAmount.grossTotal;
          totalRevenueNet += primaryAmount.netTotal;
        }
      }

      ordersByStatus[order.statusId] = (ordersByStatus[order.statusId] || 0) + 1;
      ordersByReferrer[order.referrerId] =
        (ordersByReferrer[order.referrerId] || 0) + 1;
    }

    return {
      totalOrders: orders.length,
      totalRevenue,
      totalRevenueNet,
      averageOrderValue: orders.length > 0 ? totalRevenue / orders.length : 0,
      currency: 'EUR',
      ordersByStatus,
      ordersByReferrer,
    };
  }

  /**
   * Get revenue statistics grouped by date
   */
  async getRevenueStats(
    query: PlentyoneStatsQuery,
  ): Promise<PlentyoneRevenueStats[]> {
    const orders = await this.getAllOrdersInRange(
      new Date(query.dateFrom),
      new Date(query.dateTo),
      query.statusId,
    );

    const statsByDate = new Map<string, PlentyoneRevenueStats>();

    for (const order of orders) {
      const dateKey = this.getDateKey(order.createdAt, query.groupBy || 'day');

      if (!statsByDate.has(dateKey)) {
        statsByDate.set(dateKey, {
          date: dateKey,
          ordersCount: 0,
          itemsCount: 0,
          revenue: 0,
          revenueNet: 0,
          currency: 'EUR',
        });
      }

      const stats = statsByDate.get(dateKey)!;
      stats.ordersCount++;

      if (order.orderItems?.length) {
        stats.itemsCount += order.orderItems.reduce(
          (sum, item) => sum + item.quantity,
          0,
        );
      }

      if (order.amounts?.length) {
        const primaryAmount = order.amounts.find((a) => a.isSystemCurrency);
        if (primaryAmount) {
          stats.revenue += primaryAmount.grossTotal;
          stats.revenueNet += primaryAmount.netTotal;
        }
      }
    }

    return Array.from(statsByDate.values()).sort((a, b) =>
      a.date.localeCompare(b.date),
    );
  }

  /**
   * Get all orders within a date range (handles pagination)
   */
  private async getAllOrdersInRange(
    dateFrom: Date,
    dateTo: Date,
    statusId?: number,
  ): Promise<PlentyoneOrder[]> {
    const allOrders: PlentyoneOrder[] = [];
    let page = 1;
    let hasMore = true;

    while (hasMore) {
      const query: PlentyoneOrdersQuery = {
        createdAtFrom: dateFrom.toISOString(),
        createdAtTo: dateTo.toISOString(),
        page,
        itemsPerPage: 250,
        with: 'amounts,orderItems',
      };

      if (statusId) {
        query.statusId = statusId;
      }

      const response = await this.getOrders(query);
      allOrders.push(...response.entries);

      hasMore = !response.isLastPage;
      page++;

      if (page > 1000) {
        this.logger.warn('Reached maximum page limit for order retrieval');
        break;
      }
    }

    return allOrders;
  }

  /**
   * Get date key for grouping
   */
  private getDateKey(
    dateStr: string,
    groupBy: 'day' | 'week' | 'month',
  ): string {
    const date = new Date(dateStr);

    switch (groupBy) {
      case 'day':
        return date.toISOString().split('T')[0];
      case 'week': {
        const weekStart = new Date(date);
        weekStart.setDate(date.getDate() - date.getDay());
        return weekStart.toISOString().split('T')[0];
      }
      case 'month':
        return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
      default:
        return date.toISOString().split('T')[0];
    }
  }

  /**
   * Build query parameters from object
   */
  private buildQueryParams(
    query?: object,
  ): Record<string, string> {
    if (!query) return {};

    const params: Record<string, string> = {};
    for (const [key, value] of Object.entries(query)) {
      if (value !== undefined && value !== null) {
        params[key] = String(value);
      }
    }
    return params;
  }
}
