import { Injectable } from '@nestjs/common';

/**
 * Thin Shopify Admin API client (custom-app token model).
 * Configure via SHOPIFY_SHOP + SHOPIFY_ADMIN_TOKEN in .env.
 */
@Injectable()
export class ShopifyService {
  private get shop() {
    return process.env.SHOPIFY_SHOP || '';
  }
  private get token() {
    return process.env.SHOPIFY_ADMIN_TOKEN || '';
  }
  private get version() {
    return process.env.SHOPIFY_API_VERSION || '2026-07';
  }

  configured() {
    return Boolean(this.shop && this.token);
  }

  shopDomain() {
    return this.shop;
  }

  private async req(path: string, init?: RequestInit) {
    const res = await fetch(`https://${this.shop}/admin/api/${this.version}/${path}`, {
      ...init,
      headers: {
        'X-Shopify-Access-Token': this.token,
        'Content-Type': 'application/json',
        ...(init?.headers || {}),
      },
    });
    const body = await res.json().catch(() => null);
    if (!res.ok) {
      const msg = body?.errors ? JSON.stringify(body.errors) : `Shopify API ${res.status}`;
      throw new Error(msg);
    }
    return body;
  }

  /**
   * Add tags to an order (attribution audit trail). Accepts an explicit
   * shop+token so it works per-merchant from webhooks; falls back to the env
   * store otherwise. Fail-soft — returns false on any error.
   */
  async tagOrder(orderId: string, tags: string[], shop?: string, token?: string): Promise<boolean> {
    const useShop = shop || this.shop;
    const useToken = token || this.token;
    if (!useShop || !useToken || !tags.length) return false;
    const gid = orderId.startsWith('gid://') ? orderId : `gid://shopify/Order/${orderId}`;
    try {
      const res = await fetch(`https://${useShop}/admin/api/${this.version}/graphql.json`, {
        method: 'POST',
        headers: { 'X-Shopify-Access-Token': useToken, 'Content-Type': 'application/json' },
        body: JSON.stringify({
          query: 'mutation($id: ID!, $tags: [String!]!){ tagsAdd(id: $id, tags: $tags){ userErrors{ message } } }',
          variables: { id: gid, tags },
        }),
      });
      return res.ok;
    } catch {
      return false;
    }
  }

  /**
   * List storefront products (for the affiliate deep-link picker). Per-shop
   * token supported. Returns id/title/handle + the public product URL.
   */
  async listProducts(
    opts?: { query?: string; limit?: number; shop?: string; token?: string },
  ): Promise<{ title: string; handle: string; url: string }[]> {
    const useShop = opts?.shop || this.shop;
    const useToken = opts?.token || this.token;
    if (!useShop || !useToken) return [];
    try {
      const res = await fetch(`https://${useShop}/admin/api/${this.version}/graphql.json`, {
        method: 'POST',
        headers: { 'X-Shopify-Access-Token': useToken, 'Content-Type': 'application/json' },
        body: JSON.stringify({
          query: `query($n: Int!, $q: String){ products(first: $n, query: $q){ edges{ node{ title handle onlineStoreUrl } } } }`,
          variables: { n: Math.min(opts?.limit ?? 30, 50), q: opts?.query || null },
        }),
      });
      const body = (await res.json().catch(() => null)) as any;
      const edges: any[] = body?.data?.products?.edges ?? [];
      return edges.map((e) => ({
        title: String(e.node?.title ?? ''),
        handle: String(e.node?.handle ?? ''),
        url: e.node?.onlineStoreUrl || `https://${useShop}/products/${e.node?.handle ?? ''}`,
      }));
    } catch {
      return [];
    }
  }

  /**
   * Create a real Shopify gift card (store-credit payout). Returns the one-time
   * code. Needs the `write_gift_cards` scope + Gift Cards enabled on the plan;
   * callers fall back to a discount code when it errors.
   */
  async createGiftCard(
    amount: number,
    note: string,
    shop?: string,
    token?: string,
  ): Promise<{ code?: string; error?: string }> {
    const useShop = shop || this.shop;
    const useToken = token || this.token;
    if (!useShop || !useToken) return { error: 'not configured' };
    try {
      const res = await fetch(`https://${useShop}/admin/api/${this.version}/graphql.json`, {
        method: 'POST',
        headers: { 'X-Shopify-Access-Token': useToken, 'Content-Type': 'application/json' },
        body: JSON.stringify({
          query: `mutation($input: GiftCardCreateInput!){ giftCardCreate(input: $input){ giftCardCode userErrors{ message } } }`,
          variables: { input: { initialValue: String(amount), note } },
        }),
      });
      const body = (await res.json().catch(() => null)) as any;
      const errs = body?.data?.giftCardCreate?.userErrors ?? [];
      if (errs.length) return { error: errs.map((e: { message: string }) => e.message).join('; ') };
      const code = body?.data?.giftCardCreate?.giftCardCode;
      return code ? { code } : { error: 'no code returned' };
    } catch (e) {
      return { error: (e as Error).message };
    }
  }

  /** Quick connectivity check — returns the shop's name. Per-shop token supported. */
  async shopInfo(shop?: string, token?: string): Promise<{ name: string; domain: string }> {
    const data = await this.graphql(`{ shop { name myshopifyDomain } }`, {}, shop, token);
    return { name: data?.shop?.name, domain: data?.shop?.myshopifyDomain };
  }

  /**
   * Run a GraphQL Admin API query. A per-merchant shop+token can be passed so it
   * works for any installed store from the embedded app; falls back to the env
   * store otherwise.
   */
  private async graphql(query: string, variables?: Record<string, unknown>, shop?: string, token?: string) {
    const useShop = shop || this.shop;
    const useToken = token || this.token;
    if (!useShop || !useToken) throw new Error('Shopify not connected for this store');
    const res = await fetch(`https://${useShop}/admin/api/${this.version}/graphql.json`, {
      method: 'POST',
      headers: { 'X-Shopify-Access-Token': useToken, 'Content-Type': 'application/json' },
      body: JSON.stringify({ query, variables }),
    });
    const b = (await res.json().catch(() => null)) as any;
    if (!res.ok) throw new Error(b?.errors ? JSON.stringify(b.errors) : `Shopify API ${res.status}`);
    if (b?.errors) throw new Error(JSON.stringify(b.errors));
    return b.data;
  }

  /**
   * Create a real discount code in the store (GraphQL discountCodeBasicCreate).
   * Needs only the `write_discounts` scope. Accepts a per-merchant shop+token so
   * each installed store's affiliate coupons are created in THAT store.
   */
  async createDiscountCode(
    code: string,
    type: 'PERCENT' | 'FIXED',
    value: number,
    opts?: {
      usageLimit?: number | null;
      oncePerCustomer?: boolean;
      minOrderValue?: number | null;
      minCartQty?: number | null;
      endsAt?: string | null;
      combines?: { product?: boolean; order?: boolean; shipping?: boolean } | null;
      shop?: string;
      token?: string;
    },
  ) {
    const customerValue =
      type === 'PERCENT'
        ? { percentage: Math.abs(value) / 100 }
        : { discountAmount: { amount: Math.abs(value), appliesOnEachItem: false } };

    const input: any = {
      title: code,
      code,
      startsAt: '2020-01-01T00:00:00Z',
      customerSelection: { all: true },
      customerGets: { value: customerValue, items: { all: true } },
      appliesOncePerCustomer: !!opts?.oncePerCustomer,
    };
    if (opts?.usageLimit && opts.usageLimit > 0) input.usageLimit = opts.usageLimit;
    // Shopify allows a subtotal OR quantity minimum (not both) — prefer subtotal.
    if (opts?.minOrderValue && opts.minOrderValue > 0)
      input.minimumRequirement = { subtotal: { greaterThanOrEqualToSubtotal: String(opts.minOrderValue) } };
    else if (opts?.minCartQty && opts.minCartQty > 0)
      input.minimumRequirement = { quantity: { greaterThanOrEqualToQuantity: String(opts.minCartQty) } };
    if (opts?.endsAt) input.endsAt = opts.endsAt;
    if (opts?.combines)
      input.combinesWith = {
        productDiscounts: !!opts.combines.product,
        orderDiscounts: !!opts.combines.order,
        shippingDiscounts: !!opts.combines.shipping,
      };

    const data = await this.graphql(
      `mutation Create($input: DiscountCodeBasicInput!) {
        discountCodeBasicCreate(basicCodeDiscount: $input) {
          codeDiscountNode { id }
          userErrors { field message }
        }
      }`,
      { input },
      opts?.shop,
      opts?.token,
    );
    const errs = data?.discountCodeBasicCreate?.userErrors ?? [];
    if (errs.length) throw new Error(errs.map((e: { message: string }) => e.message).join('; '));
    return { id: data?.discountCodeBasicCreate?.codeDiscountNode?.id, code };
  }

  /**
   * Recent orders (with discount codes) for attribution — via GraphQL.
   * GraphQL is more reliable than REST here (REST Orders is gated by
   * protected-customer-data rules and can return empty). We avoid requesting
   * customer PII so no extra data-protection approval is needed.
   */
  async listOrders(limit = 100, shop?: string, token?: string): Promise<
    {
      id: string;
      email: string | null;
      subtotal_price: string;
      total_price: string;
      created_at: string;
      discount_codes: { code: string }[];
      line_titles: string[];
      line_items: { title: string; vendor: string; amount: number }[];
      tags: string[];
    }[]
  > {
    const data = await this.graphql(
      `query Orders($n: Int!) {
        orders(first: $n, sortKey: CREATED_AT, reverse: true) {
          edges {
            node {
              id
              createdAt
              discountCodes
              tags
              subtotalPriceSet { shopMoney { amount } }
              totalPriceSet { shopMoney { amount } }
              lineItems(first: 50) { edges { node { title vendor originalTotalSet { shopMoney { amount } } } } }
            }
          }
        }
      }`,
      { n: limit },
      shop,
      token,
    );
    const edges: any[] = data?.orders?.edges ?? [];
    return edges.map((e) => {
      const lines = (e.node.lineItems?.edges ?? []).map((li: any) => ({
        title: String(li.node?.title ?? ''),
        vendor: String(li.node?.vendor ?? ''),
        amount: Number(li.node?.originalTotalSet?.shopMoney?.amount ?? 0),
      }));
      return {
        id: String(e.node.id).split('/').pop() ?? String(e.node.id),
        email: null,
        subtotal_price: e.node.subtotalPriceSet?.shopMoney?.amount ?? '0',
        total_price: e.node.totalPriceSet?.shopMoney?.amount ?? '0',
        created_at: e.node.createdAt,
        discount_codes: (e.node.discountCodes ?? []).map((c: string) => ({ code: c })),
        line_titles: lines.map((l: { title: string }) => l.title),
        line_items: lines,
        tags: e.node.tags ?? [],
      };
    });
  }
}
