/**
 * FedEx Track API client (OAuth 2.0 + track/v1/trackingnumbers).
 */

import axios from 'axios';
import { Mutex } from 'async-mutex';
import qs from 'querystring';
import { loadSecrets } from '../../config/secrets.js';
import { logger } from '../../utils/logger.js';

const secrets = loadSecrets();
const mutex = new Mutex();

let cachedToken = null;
let tokenExpiresAt = 0;

function getApiBase() {
  return (process.env.FEDEX_API_BASE || secrets.fedex?.apiBase || 'https://apis.fedex.com').replace(/\/$/, '');
}

export function isFedExConfigured() {
  const clientId = process.env.FEDEX_CLIENT_ID || secrets.fedex?.clientId;
  const clientSecret = process.env.FEDEX_CLIENT_SECRET || secrets.fedex?.clientSecret;
  return Boolean(clientId && clientSecret);
}

export function isShipmentTrackingEnabled() {
  const raw = process.env.SHIPMENT_TRACKING_ENABLED;
  if (raw == null || raw === '') return isFedExConfigured();
  return !/^(0|false|no|off)$/i.test(String(raw).trim());
}

async function getFedExToken(forceRefresh = false) {
  const clientId = process.env.FEDEX_CLIENT_ID || secrets.fedex?.clientId;
  const clientSecret = process.env.FEDEX_CLIENT_SECRET || secrets.fedex?.clientSecret;

  if (!clientId || !clientSecret) {
    throw new Error('FedEx credentials not configured (FEDEX_CLIENT_ID, FEDEX_CLIENT_SECRET)');
  }

  const release = await mutex.acquire();
  try {
    const now = Date.now();
    if (!forceRefresh && cachedToken && now < tokenExpiresAt) {
      return cachedToken;
    }

    const base = getApiBase();
    const response = await axios.post(
      `${base}/oauth/token`,
      qs.stringify({
        grant_type: 'client_credentials',
        client_id: clientId,
        client_secret: clientSecret,
      }),
      {
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        timeout: 15000,
      }
    );

    const { access_token, expires_in } = response.data;
    cachedToken = access_token;
    tokenExpiresAt = now + (Number(expires_in) || 3600) * 1000 - 300_000;
    logger('fedex:oauth', 'New FedEx access token acquired');
    return access_token;
  } catch (err) {
    const msg = err.response?.data
      ? JSON.stringify(err.response.data)
      : err.message;
    logger('fedex:oauth', `Token fetch failed: ${msg}`, 'error');
    throw new Error(`FedEx OAuth failed: ${msg}`);
  } finally {
    release();
  }
}

const FEDEX_STATUS_LABELS = {
  DL: 'Delivered',
  OD: 'Out for delivery',
  IT: 'On the way',
  PU: 'Picked up',
  OC: 'Label created',
  SE: 'Shipment exception',
  CA: 'Canceled',
};

function formatScanLocation(loc) {
  if (!loc) return '';
  const parts = [loc.city, loc.stateOrProvinceCode, loc.countryCode === 'US' ? null : loc.countryCode]
    .filter(Boolean);
  return parts.join(', ');
}

function pickEstimatedDelivery(trackResult) {
  const edtw = trackResult?.estimatedDeliveryTimeWindow?.window;
  if (edtw?.ends) {
    return { start: edtw.begins || null, end: edtw.ends };
  }

  const std = trackResult?.standardTransitTimeWindow?.window;
  if (std?.ends) {
    return { start: std.begins || null, end: std.ends };
  }

  const dateTimes = trackResult?.dateAndTimes || [];
  const estimates = dateTimes
    .filter((d) => String(d.type || d.dateTimeType || '').toUpperCase() === 'ESTIMATED_DELIVERY')
    .map((d) => d.dateTime)
    .filter(Boolean)
    .sort((a, b) => new Date(b).getTime() - new Date(a).getTime());

  if (estimates.length) {
    return { start: null, end: estimates[0] };
  }

  return { start: null, end: null };
}

export function formatFedExEta(estimatedDelivery, estimatedDeliveryStart = null) {
  if (!estimatedDelivery) return null;

  const end = new Date(estimatedDelivery);
  if (Number.isNaN(end.getTime())) return null;

  if (estimatedDeliveryStart) {
    const start = new Date(estimatedDeliveryStart);
    if (!Number.isNaN(start.getTime()) && start.toDateString() === end.toDateString()) {
      const fmt = { hour: 'numeric', minute: '2-digit' };
      return `${end.toLocaleDateString('en-US')}, ${start.toLocaleTimeString('en-US', fmt)}–${end.toLocaleTimeString('en-US', fmt)}`;
    }
  }

  return end.toLocaleDateString('en-US');
}

export function formatFedExStatusLine({
  statusDescription = '',
  statusCode = '',
  estimatedDelivery = null,
  estimatedDeliveryStart = null,
  deliveredAt = null,
} = {}) {
  const parts = [];
  if (statusDescription) parts.push(statusDescription);
  else if (statusCode) parts.push(FEDEX_STATUS_LABELS[statusCode] || statusCode);
  else parts.push('Status unknown');

  const eta = formatFedExEta(estimatedDelivery, estimatedDeliveryStart);
  if (eta) parts.push(`ETA ${eta}`);

  if (deliveredAt) {
    parts.push(`Delivered ${new Date(deliveredAt).toLocaleDateString('en-US')}`);
  }

  return parts.join(' · ');
}

function buildStatusDescription(detail, latestScan, statusCode) {
  const headline =
    detail.statusByLocale ||
    FEDEX_STATUS_LABELS[statusCode] ||
    detail.description ||
    latestScan?.derivedStatus ||
    latestScan?.eventDescription ||
    '';

  const scanText = latestScan?.eventDescription || '';
  const scanLoc = formatScanLocation(latestScan?.scanLocation || detail.scanLocation);
  let scanDetail = scanText;
  if (scanText && scanLoc && !scanText.toUpperCase().includes(scanLoc.toUpperCase())) {
    scanDetail = `${scanText}, ${scanLoc}`;
  } else if (!scanText && scanLoc) {
    scanDetail = scanLoc;
  }

  if (!headline) return scanDetail || 'Status unknown';
  if (!scanDetail || scanDetail.toLowerCase() === headline.toLowerCase()) return headline;
  if (headline.toLowerCase().includes(scanDetail.toLowerCase())) return headline;

  return `${headline} · ${scanDetail}`;
}

function pickLatestStatus(trackResult) {
  const detail = trackResult?.latestStatusDetail || {};
  const scanEvents = trackResult?.scanEvents || [];
  const latestScan = scanEvents.length > 0 ? scanEvents[0] : null;

  const statusCode = String(detail.code || detail.derivedCode || latestScan?.eventType || '')
    .trim()
    .toUpperCase();
  const statusDescription = buildStatusDescription(detail, latestScan, statusCode);
  const { start: estimatedDeliveryStart, end: estimatedDelivery } = pickEstimatedDelivery(trackResult);

  let deliveredAt = null;
  if (statusCode === 'DL' || /delivered/i.test(statusDescription)) {
    const deliveredEvent = scanEvents.find((e) =>
      String(e.eventType || '').toUpperCase() === 'DL' || /delivered/i.test(e.eventDescription || '')
    );
    deliveredAt = deliveredEvent?.date || detail.ancillaryDetails?.[0]?.value || new Date().toISOString();
  }

  return {
    statusCode,
    statusDescription,
    estimatedDelivery,
    estimatedDeliveryStart,
    deliveredAt,
  };
}

function normalizeTrackResult(trackingNumber, completeTrackResult) {
  const trackResults = completeTrackResult?.trackResults || [];
  const trackResult = trackResults[0] || null;

  if (!trackResult) {
    const error = completeTrackResult?.error || completeTrackResult?.alerts?.[0];
    return {
      trackingNumber,
      ok: false,
      error: error?.message || error?.code || 'No track results returned',
    };
  }

  const status = pickLatestStatus(trackResult);
  return {
    trackingNumber,
    ok: true,
    ...status,
  };
}

/**
 * Track up to 30 FedEx tracking numbers in one API call.
 * @returns {Promise<Map<string, object>>} trackingNumber → result
 */
export async function trackByNumbers(trackingNumbers = [], { _retried = false } = {}) {
  const nums = [...new Set(trackingNumbers.map((n) => String(n).trim()).filter(Boolean))];
  if (!nums.length) return new Map();

  const token = await getFedExToken(_retried);
  const base = getApiBase();
  const accountNumber = process.env.FEDEX_ACCOUNT_NUMBER || secrets.fedex?.accountNumber || null;

  const body = {
    includeDetailedScans: true,
    trackingInfo: nums.map((trackingNumber) => ({
      trackingNumberInfo: { trackingNumber },
    })),
  };

  if (accountNumber) {
    body.shipperAccountNumber = { value: String(accountNumber) };
  }

  try {
    const response = await axios.post(`${base}/track/v1/trackingnumbers`, body, {
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
        'X-locale': 'en_US',
      },
      timeout: 30000,
    });
    const results = new Map();
    const complete = response.data?.output?.completeTrackResults || [];

    for (const entry of complete) {
      const num = entry?.trackingNumber || entry?.trackResults?.[0]?.trackingNumberInfo?.trackingNumber;
      if (!num) continue;
      results.set(String(num), normalizeTrackResult(String(num), entry));
    }

    for (const num of nums) {
      if (!results.has(num)) {
        results.set(num, { trackingNumber: num, ok: false, error: 'Not returned by FedEx API' });
      }
    }

    return results;
  } catch (err) {
    if (err.response?.status === 401 && !_retried) {
      await getFedExToken(true);
      return trackByNumbers(nums, { _retried: true });
    }
    const msg = err.response?.data ? JSON.stringify(err.response.data) : err.message;
    logger('fedex:track', `Track request failed: ${msg}`, 'error');
    throw new Error(`FedEx track failed: ${msg}`);
  }
}

export default { isFedExConfigured, isShipmentTrackingEnabled, trackByNumbers, formatFedExEta, formatFedExStatusLine };
