import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable, timer } from 'rxjs';
import { map, retry } from 'rxjs/operators';
import { CategoryApiModel, DeliveryOption, Item, Subcategory } from '../models';
import { normalizeDeliveryOption, normalizeOptionalNumber } from '../utils/normalization.utils';
import { ApiConfigService } from '../core/config/api-config.service';

/**
 * Server-authoritative checkout. Contract: PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §5.2.
 * No `amount` or `price` field anywhere in this pair - the backend prices
 * every offer itself from its own catalog and the current FX quote.
 */
export interface CheckoutSessionRequest {
  offers: Array<{ offerId: string; qty: number }>;
  currency: string;
  deliveryOptionId?: string;
}

interface MoneyAmount {
  amountMinor: number;
  currency: string;
}

export interface CheckoutSessionLine {
  offerId: string;
  qty: number;
  unitPrice: MoneyAmount;
  lineTotal: MoneyAmount;
  priceSnapshotId: string;
}

export interface CheckoutSessionResponse {
  checkoutSessionId: string;
  lines: CheckoutSessionLine[];
  subtotal: MoneyAmount;
  discount: MoneyAmount;
  delivery: MoneyAmount;
  total: MoneyAmount;
  fxQuoteId: string;
  expiresAt: string;
}

export interface CreateOrderRequest {
  /**
   * No `price` field: the backend must price each line item from its own
   * catalog by `productId`, never trust a client-supplied amount.
   * See BACKEND-API-REFERENCE.md §12.
   */
  items: Array<{ productId: string; name: string; quantity: number }>;
  customer: { name: string; email: string; phone: string };
  payment?: { method: string; currency: string };
  shipping?: { address: string; method: string; trackingNumber: string };
}

export interface CreateOrderResponse {
  id: string;
  orderNumber: string;
  status: string;
  total: number;
  currency: string;
}

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  private readonly retryConfig = {
    count: 2,
    delay: (_error: unknown, retryCount: number) => timer(Math.pow(2, retryCount) * 500)
  };

  constructor(
    private readonly http: HttpClient,
    private readonly apiConfig: ApiConfigService
  ) {}

  private get baseUrl(): string {
    return this.apiConfig.getBaseUrl();
  }

  /** Map API language codes (RU/EN/AM) → frontend codes (ru/en/hy) */
  private normalizeLang(apiLang: string): string {
    const map: Record<string, string> = { 'RU': 'ru', 'EN': 'en', 'AM': 'hy' };
    return map[apiLang] || apiLang.toLowerCase();
  }

  /** Convert Go-style hex colour (0xfffca0) → CSS hex (#fffca0) */
  private normalizeColor(c: string): string {
    if (!c) return '';
    return c.startsWith('0x') ? '#' + c.slice(2) : c;
  }

  private normalizeMediaType(type: unknown): 'image' | 'video' | 'pdf' | 'manual' | 'warranty' {
    const value = String(type ?? '').toLowerCase();
    if (value === 'video') return 'video';
    if (value === 'pdf') return 'pdf';
    if (value === 'manual') return 'manual';
    if (value === 'warranty') return 'warranty';
    return 'image';
  }

  private normalizeDeliveryData(
    raw: any,
    legacyDeliveryPrice?: number
  ): { options: DeliveryOption[]; isDigital: boolean; requiresSelection: boolean } {
    const rawDelivery = raw.delivery ?? raw.deliveries;

    if (typeof rawDelivery === 'string' && rawDelivery.trim().toLowerCase() === 'digital') {
      return { options: [], isDigital: true, requiresSelection: false };
    }

    const deliveryCandidates = Array.isArray(rawDelivery)
      ? rawDelivery
      : rawDelivery != null
        ? [rawDelivery]
        : [];
    const options = deliveryCandidates
      .map(candidate => normalizeDeliveryOption(candidate))
      .filter((option): option is DeliveryOption => option !== null);

    if (options.length > 0) {
      return { options, isDigital: false, requiresSelection: rawDelivery != null };
    }

    if (legacyDeliveryPrice !== undefined) {
      return {
        options: [{ deliveryPrice: legacyDeliveryPrice, deliveryPlace: '', deliveryTime: '' }],
        isDigital: false,
        requiresSelection: false,
      };
    }

    if (rawDelivery != null) {
      return { options: [], isDigital: true, requiresSelection: false };
    }

    return { options: [], isDigital: false, requiresSelection: false };
  }

  private normalizeSubcategory(raw: any): Subcategory {
    const subcategory: Subcategory = {
      id: String(raw.id ?? raw.categoryId ?? raw.categoryID ?? ''),
      name: typeof raw.name === 'string' ? raw.name : '',
      visible: raw.visible ?? true,
      priority: raw.priority ?? 0,
      img: raw.img ? this.resolveImageUrl(raw.img) : undefined,
      categoryId: String(raw.categoryId ?? raw.categoryID ?? raw.id ?? ''),
      parentId: String(raw.parentId ?? raw.parentID ?? ''),
      itemCount: raw.itemCount ?? raw.ItemsCount ?? 0,
      hasItems: raw.hasItems,
      subcategories: Array.isArray(raw.subcategories)
        ? raw.subcategories
            .map((sub: any) => this.normalizeSubcategory(sub))
            .filter((sub: Subcategory) => this.isDisplayableSubcategory(sub))
        : [],
    };

    return subcategory;
  }

  private isDisplayableSubcategory(subcategory: Subcategory): boolean {
    if (subcategory.visible === false) {
      return false;
    }

    return (subcategory.itemCount ?? 0) > 0
      || subcategory.hasItems === true
      || (subcategory.subcategories?.length ?? 0) > 0;
  }

  private isDisplayableCategory(category: CategoryApiModel): boolean {
    return category.visible !== false;
  }

  private isDisplayableItem(item: Item): boolean {
    return item.visible !== false;
  }

  /** Resolve relative image URLs (e.g. ./images/x.webp) against site origin */
  private resolveImageUrl(url: string): string {
    if (!url) return '';
    if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/')) return url;
    const origin = typeof window !== 'undefined'
      ? window.location.origin
      : '';
    if (url.startsWith('./')) return `${origin}/${url.slice(2)}`;
    return `${origin}/${url}`;
  }

  /**
   * Normalize an item from the API response — supports both
   * legacy marketplace format and the new backOffice API format.
   */
  private normalizeItem(raw: any): Item {
    const { partnerID, ...rest } = raw;
    const item: Item = { ...rest };
    let legacyDeliveryPrice = normalizeOptionalNumber(
      raw.deliveryPrice ?? raw.delivery_price ?? raw.deliveryprice
    );

    // Extract price/currency/remaining/colour/size from itemDetails[]
    // Note: Go struct tag is "itemdetails" but actual API may send "itemDetails"
    const details = raw.itemDetails || raw.itemdetails;
    if (details && Array.isArray(details) && details.length > 0) {
      const detail = details[0];
      item.itemDetails = details.map((d: any) => ({
        ...d,
        colour: this.normalizeColor(d.colour || d.color || ''),
        color: undefined,
        deliveryPrice: normalizeOptionalNumber(
          d.deliveryPrice ?? d.delivery_price ?? d.deliveryprice
        ),
      }));
      if (item.price == null || item.price === 0) item.price = detail.price;
      if (!item.currency) item.currency = detail.currency;
      if (!item.colour) item.colour = this.normalizeColor(detail.colour || detail.color || '');
      if (!item.size) item.size = detail.size || '';
      if (legacyDeliveryPrice === undefined) {
        legacyDeliveryPrice = normalizeOptionalNumber(
          detail.deliveryPrice ?? detail.delivery_price ?? detail.deliveryprice
        );
      }
      // Use remaining from detail for stock level
      if (raw.remaining == null && detail.remaining != null) {
        (raw as any).remaining = detail.remaining;
      }
    }
    const deliveryData = this.normalizeDeliveryData(raw, legacyDeliveryPrice);
    if (deliveryData.options.length > 0) {
      item.deliveryOptions = deliveryData.options;
      item.deliveryMode = 'selectable';
      item.deliverySelectionRequired = deliveryData.requiresSelection;
    } else if (deliveryData.isDigital) {
      item.deliveryMode = 'digital';
      item.deliverySelectionRequired = false;
    }

    // Map backOffice string id → legacy numeric itemID
    if (raw.id != null && raw.itemID == null) {
      item.id = String(raw.id);
      item.itemID = typeof raw.id === 'number' ? raw.id : 0;
    }

    // Map backOffice imgs[] → legacy photos[]
    if (raw.imgs && (!raw.photos || raw.photos.length === 0)) {
      item.photos = raw.imgs.map((url: string) => ({ url }));
    }
    // Normalize photo type: API sends type='video'|'photo', template checks .video
    // Also resolve relative URLs (e.g. ./images/x.webp) against API base
    if (item.photos) {
      item.photos = item.photos.map((p: any) => ({
        ...p,
        url: this.resolveImageUrl(p.url),
        video: p.video || (p.type === 'video' ? p.url : undefined),
      }));
    }
    item.imgs = raw.imgs?.map((u: string) => this.resolveImageUrl(u))
      || item.photos?.map((p: any) => p.url) || [];

    const rawMedia = Array.isArray(raw.media) ? raw.media : [];
    const mediaFromPhotos = (item.photos ?? []).map((photo: any, index: number) => ({
      id: photo.id ?? `photo-${item.itemID}-${index}`,
      type: this.normalizeMediaType(photo.type ?? (photo.video ? 'video' : 'image')),
      url: this.resolveImageUrl(photo.url),
      thumbnailUrl: photo.thumbnailUrl ? this.resolveImageUrl(photo.thumbnailUrl) : undefined,
      alt: photo.alt,
      title: photo.title,
      labels: photo.labels
    }));

    item.media = (rawMedia.length > 0 ? rawMedia : mediaFromPhotos)
      .map((entry: any, index: number) => ({
        id: entry.id ?? `media-${item.itemID}-${index}`,
        type: this.normalizeMediaType(entry.type),
        url: this.resolveImageUrl(entry.url),
        thumbnailUrl: entry.thumbnailUrl ? this.resolveImageUrl(entry.thumbnailUrl) : undefined,
        alt: entry.alt,
        title: entry.title,
        labels: entry.labels,
      }))
      .filter((entry: any) => !!entry.url);

    // Map backOffice description (key-value array) → legacy description string
    if (Array.isArray(raw.description)) {
      item.descriptionFields = raw.description;
      item.description = raw.description.map((d: any) => `${d.key}: ${d.value}`).join('\n');
    } else {
      item.description = raw.description || raw.simpleDescription || '';
    }

    // Map backend names[] → translations (multi-lang name support)
    // Note: API has typo "valuue" in some responses, handle both
    if (raw.names && Array.isArray(raw.names)) {
      item.names = raw.names;
      if (!item.translations) item.translations = {};
      for (const entry of raw.names) {
        const lang = this.normalizeLang(entry.language);
        const val = entry.value || entry.valuue || '';
        if (val) {
          if (!item.translations[lang]) item.translations[lang] = {};
          item.translations[lang].name = val;
        }
      }
      // Fallback: if top-level name is missing, use first available translation
      if (!item.name && raw.names.length > 0) {
        const ruName = raw.names.find((n: any) => n.language === 'RU' || n.language === 'ru');
        item.name = ruName?.value || ruName?.valuue || raw.names[0].value || raw.names[0].valuue || '';
      }
    }

    // Preserve attributes from backend
    item.attributes = raw.attributes || [];

    item.specificationGroups = Array.isArray(raw.specificationGroups)
      ? raw.specificationGroups.map((group: any, groupIndex: number) => ({
          id: group.id ?? `group-${groupIndex}`,
          key: group.key ?? `group-${groupIndex}`,
          label: group.label,
          labels: group.labels,
          attributes: Array.isArray(group.attributes)
            ? group.attributes.map((attribute: any, attributeIndex: number) => ({
                key: attribute.key ?? `attribute-${attributeIndex}`,
                value: String(attribute.value ?? ''),
                label: attribute.label,
                labels: attribute.labels,
                unit: attribute.unit,
              }))
            : []
        }))
      : [];

    item.variantOptions = Array.isArray(raw.variantOptions)
      ? raw.variantOptions.map((group: any, groupIndex: number) => ({
          key: group.key ?? `variant-${groupIndex}`,
          label: group.label,
          labels: group.labels,
          options: Array.isArray(group.options)
            ? group.options.map((option: any) => ({
                value: String(option.value ?? ''),
                label: option.label,
                labels: option.labels,
                available: option.available !== false,
              })).filter((option: any) => option.value.length > 0)
            : []
        })).filter((group: any) => group.options.length > 0)
      : [];

    item.relatedCollections = Array.isArray(raw.relatedCollections)
      ? raw.relatedCollections.map((collection: any, collectionIndex: number) => ({
          id: String(collection.id ?? `related-${collectionIndex}`),
          title: String(collection.title ?? ''),
          titles: collection.titles,
          products: Array.isArray(collection.products)
            ? collection.products.map((productId: any) => Number(productId)).filter((productId: number) => Number.isFinite(productId))
            : []
        }))
      : [];

    // Preserve colour & size (only if not already set from itemDetails)
    if (!item.colour) item.colour = this.normalizeColor(raw.colour || '');
    if (!item.size) item.size = raw.size || '';

    // Map backOffice comments → legacy callbacks
    if (raw.comments && (!raw.callbacks || raw.callbacks.length === 0)) {
      item.callbacks = raw.comments.map((c: any) => ({
        rating: c.stars,
        content: c.text,
        userID: c.author,
        timestamp: c.createdAt,
      }));
    }
    item.comments = raw.comments || raw.callbacks?.map((c: any) => ({
      id: c.userID,
      text: c.content,
      author: c.userID,
      stars: c.rating,
      createdAt: c.timestamp,
    })) || [];

    // Compute average rating from comments if not present
    if (raw.rating == null && item.comments && item.comments.length > 0) {
      const rated = item.comments.filter(c => c.stars != null);
      item.rating = rated.length > 0
        ? rated.reduce((sum, c) => sum + (c.stars || 0), 0) / rated.length
        : 0;
    }
    item.rating = item.rating || 0;

    // Defaults
    item.name = item.name || '';
    item.price = item.price ?? 0;
    item.discount = item.discount || 0;
    item.remainings = item.remainings || (raw.remaining != null
      ? (raw.remaining <= 0 ? 'out' : raw.remaining <= 5 ? 'low' : raw.remaining <= 20 ? 'medium' : 'high')
      : raw.quantity != null
      ? (raw.quantity <= 0 ? 'out' : raw.quantity <= 5 ? 'low' : raw.quantity <= 20 ? 'medium' : 'high')
      : 'high');
    item.currency = item.currency || 'RUB';

    // Preserve new backOffice fields
    item.badges = raw.badges || [];
    item.tags = raw.tags || [];
    item.simpleDescription = raw.simpleDescription || '';
    item.translations = item.translations || raw.translations || {};
    item.visible = raw.visible ?? true;
    item.priority = raw.priority ?? 0;
    item.visits = raw.visits ?? 0;

    // Map question like/dislike → upvotes/downvotes
    if (item.questions) {
      item.questions = item.questions.map((q: any) => ({
        ...q,
        upvotes: q.upvotes ?? q.like ?? 0,
        downvotes: q.downvotes ?? q.dislike ?? 0,
      }));
    }

    return item;
  }

  private normalizeItems(items: any[] | null | undefined): Item[] {
    if (!items || !Array.isArray(items)) {
      return [];
    }
    return items
      .map(item => this.normalizeItem(item))
      .filter(item => this.isDisplayableItem(item));
  }

  /**
   * Normalize a category from the API response — supports both
   * the flat legacy format and nested backOffice format.
   */
  private normalizeCategory(raw: any): CategoryApiModel {
    const cat: CategoryApiModel = { ...raw };

    if (raw.id != null && raw.categoryID == null) {
      cat.id = String(raw.id);
      cat.categoryID = typeof raw.id === 'number' ? raw.id : 0;
    }

    // Map backOffice img → legacy icon
    if (raw.img && !raw.icon) {
      cat.icon = raw.img;
    }
    cat.img = raw.img || raw.icon;

    // Resolve relative icon/image URLs
    if (cat.icon) cat.icon = this.resolveImageUrl(cat.icon);
    if (cat.img) cat.img = this.resolveImageUrl(cat.img);

    // Map backend wideicon → wideBanner
    if (raw.wideicon && !cat.wideBanner) {
      cat.wideBanner = raw.wideicon;
    }

    cat.parentID = raw.parentID ?? 0;
    cat.visible = raw.visible ?? true;
    cat.priority = raw.priority ?? 0;
    cat.itemCount = raw.itemCount ?? raw.ItemsCount ?? 0;
    cat.categoriesCount = raw.categoriesCount ?? raw.CategoriesCount ?? 0;
    cat.marketplaceId = raw.marketplaceId ?? raw.projectId;

    // Map backend names[] → translations (multi-lang name support)
    // Note: API has typo "valuue" in some responses, handle both
    if (raw.names && Array.isArray(raw.names)) {
      cat.names = raw.names;
      cat.translations = cat.translations || {};
      for (const entry of raw.names) {
        const lang = this.normalizeLang(entry.language);
        const val = entry.value || entry.valuue || '';
        if (val) {
          if (!cat.translations[lang]) cat.translations[lang] = {};
          cat.translations[lang].name = val;
        }
      }
      // Fallback: if top-level name is missing, use first available translation
      if (!cat.name && raw.names.length > 0) {
        const ruName = raw.names.find((n: any) => n.language === 'RU' || n.language === 'ru');
        cat.name = ruName?.value || ruName?.valuue || raw.names[0].value || raw.names[0].valuue || '';
      }
    }
    cat.name = cat.name || '';

    if (raw.subcategories && Array.isArray(raw.subcategories)) {
      cat.subcategories = raw.subcategories
        .map((sub: any) => this.normalizeSubcategory(sub))
        .filter((sub: Subcategory) => this.isDisplayableSubcategory(sub));
    }

    return cat;
  }

  private normalizeCategories(cats: any[] | null | undefined): CategoryApiModel[] {
    if (!cats || !Array.isArray(cats)) return [];
    return cats
      .map(c => this.normalizeCategory(c))
      .filter(category => this.isDisplayableCategory(category));
  }

  // ─── Core Marketplace Endpoints ───────────────────────────

  ping(): Observable<{ message: string }> {
    return this.http.get<{ message: string }>(`${this.baseUrl}/ping`);
  }

  getCategories(): Observable<CategoryApiModel[]> {
    return this.http.get<any[]>(`${this.baseUrl}/category`)
      .pipe(retry(this.retryConfig), map(cats => this.normalizeCategories(cats)));
  }

  getCategoryItems(categoryID: number, count: number = 50, skip: number = 0): Observable<Item[]> {
    const params = new HttpParams()
      .set('count', count.toString())
      .set('skip', skip.toString());
    return this.http.get<any[]>(`${this.baseUrl}/category/${categoryID}`, { params })
      .pipe(retry(this.retryConfig), map(items => this.normalizeItems(items)));
  }

  getItem(itemID: number): Observable<Item> {
    return this.http.get<any>(`${this.baseUrl}/items/${itemID}`)
      .pipe(retry(this.retryConfig), map(item => this.normalizeItem(item)));
  }

  searchItems(
    search: string,
    count: number = 50,
    skip: number = 0,
    options?: {
      categoryIDs?: number[];
      minPrice?: number;
      maxPrice?: number;
      tag?: string;
      sort?: 'relevance' | 'price_asc' | 'price_desc' | 'popular' | 'rating';
    }
  ): Observable<{ items: Item[], total: number }> {
    let params = new HttpParams()
      .set('search', search)
      .set('count', count.toString())
      .set('skip', skip.toString());
    if (options?.categoryIDs?.length) {
      params = params.set('categoryIDs', options.categoryIDs.join(','));
    }
    if (options?.minPrice != null) {
      params = params.set('minPrice', options.minPrice.toString());
    }
    if (options?.maxPrice != null) {
      params = params.set('maxPrice', options.maxPrice.toString());
    }
    if (options?.tag) {
      params = params.set('tag', options.tag);
    }
    if (options?.sort) {
      params = params.set('sort', options.sort);
    }
    return this.http.get<any>(`${this.baseUrl}/searchitems`, { params })
      .pipe(
        retry(this.retryConfig),
        map(response => ({
          items: this.normalizeItems(response?.items || []),
          total: response?.total || 0
        }))
      );
  }

  // Cart operations — spec uses websession-based paths
  addToCart(sessionId: string, items: Array<{ itemID: number; quantity: number; colour?: string; size?: string; price?: number }>): Observable<any> {
    return this.http.post<any>(`${this.baseUrl}/websession/${sessionId}`, items);
  }

  // Review submission
  submitReview(reviewData: {
    itemID: number;
    rating: number;
    comment: string;
    sessionID: string;
    timestamp: string;
  }): Observable<{ message: string }> {
    const { itemID, ...body } = reviewData;
    return this.http.post<{ message: string }>(`${this.baseUrl}/items/${itemID}/callback`, body);
  }

  // Question submission — spec path has typo "questiion"
  submitQuestion(questionData: {
    itemID: number;
    question: string;
    sessionID: string;
    timestamp: string;
  }): Observable<{ message: string }> {
    const { itemID, ...body } = questionData;
    return this.http.post<{ message: string }>(`${this.baseUrl}/items/${itemID}/questiion`, body);
  }

  /**
   * Creates a server-priced checkout session. Contract §5.2 - the frontend
   * sends offer ids and quantities only; the response carries the total that
   * actually gets charged, computed server-side from the live offer price
   * and current FX quote.
   */
  createCheckoutSession(payload: CheckoutSessionRequest): Observable<CheckoutSessionResponse> {
    return this.http.post<CheckoutSessionResponse>('/api/v2/storefront/checkout', payload);
  }

  /**
   * Records the just-paid cart as a backoffice order (POST /orders). Fire-and-forget
   * from the caller's perspective - a failure here must never block the existing
   * payment-confirmed flow, since payment itself is unaffected by this call.
   */
  createOrder(payload: CreateOrderRequest): Observable<CreateOrderResponse> {
    return this.http.post<CreateOrderResponse>(`${this.baseUrl}/orders`, payload);
  }

  submitPurchaseEmail(emailData: {
    email: string;
    phone?: string;
    telegramUserId: string | null;
    items: Array<{ itemID: number; name: string; price: number; currency: string; quantity?: number; delivery?: DeliveryOption[] }>;
  }): Observable<{ message: string }> {
    return this.http.post<{ message: string }>(`${this.baseUrl}/purchase-email`, emailData);
  }

  /**
   * Back-in-stock subscription. No backend endpoint exists for this yet
   * (tracked in BACKEND-API-REFERENCE.md §12) - callers should fall back to
   * a local-only record (see CartService/UX facade patterns) when this 404s
   * or the request otherwise fails, rather than surfacing an error to the
   * shopper for something this minor.
   */
  subscribeToRestock(itemID: number, contact: { telegramUserId: string | null; email?: string }): Observable<void> {
    return this.http.post<void>(`${this.baseUrl}/items/${itemID}/notify-me`, contact);
  }

  getRandomItems(count: number = 5, categoryID?: number): Observable<Item[]> {
    let params = new HttpParams().set('count', count.toString());
    if (categoryID) {
      params = params.set('category', categoryID.toString());
    }
      return this.http.get<any[]>(`${this.baseUrl}/items/randomitems`, { params })
      .pipe(retry(this.retryConfig), map(items => this.normalizeItems(items)));
  }
}
