import { createHash, randomBytes } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { assertSafeRemoteUrl, decryptJson, encryptJson } from './security.mjs';

const ETSY_ROOT = 'https://api.etsy.com/v3';

const base64url = value => Buffer.from(value).toString('base64url');
const parseMoney = value => {
  if (value == null) return 0;
  if (typeof value === 'number') return value;
  if (typeof value === 'string') return Number.parseFloat(value) || 0;
  return (Number.parseFloat(value.amount) || 0) / (Number(value.divisor) || 1);
};

function requireConfig(config) {
  if (!config.etsyApiKey || !config.etsySharedSecret) {
    throw new Error('Etsy is not configured. Add ETSY_API_KEY and ETSY_SHARED_SECRET to .env.');
  }
}

function apiHeaders(config, token, json = false) {
  return {
    'x-api-key': `${config.etsyApiKey}:${config.etsySharedSecret}`,
    ...(token ? { Authorization: `Bearer ${token}` } : {}),
    ...(json ? { 'Content-Type': 'application/json' } : {}),
  };
}

async function etsyError(response, label) {
  const body = (await response.text()).slice(0, 2000);
  throw new Error(`${label} failed (${response.status}): ${body}`);
}

async function getToken(store, key) {
  const token = decryptJson(key, store.getSecret('etsy'));
  if (!token) throw new Error('Etsy is not connected.');
  return token;
}

async function saveToken(store, key, token) {
  await store.setSecret('etsy', encryptJson(key, token));
}

async function validToken(store, key, config) {
  const token = await getToken(store, key);
  if (new Date(token.expires_at).getTime() > Date.now() + 60_000) return token;
  const response = await fetch(`${ETSY_ROOT}/public/oauth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      client_id: config.etsyApiKey,
      refresh_token: token.refresh_token,
    }),
  });
  if (!response.ok) await etsyError(response, 'Etsy token refresh');
  const fresh = await response.json();
  const updated = {
    ...token,
    access_token: fresh.access_token,
    refresh_token: fresh.refresh_token || token.refresh_token,
    expires_at: new Date(Date.now() + (fresh.expires_in || 3600) * 1000).toISOString(),
  };
  await saveToken(store, key, updated);
  return updated;
}

function allowedRedirect(redirectUri, config) {
  const candidate = new URL(redirectUri);
  const allowed = new Set(config.allowedRedirectOrigins);
  if (!allowed.has(candidate.origin)) throw new Error(`Redirect origin ${candidate.origin} is not allowed`);
  return candidate.toString();
}

async function detectShopId(accessToken, config) {
  const userId = accessToken.split('.')[0];
  const response = await fetch(`${ETSY_ROOT}/application/users/${userId}/shops`, {
    headers: apiHeaders(config, accessToken),
  });
  if (!response.ok) await etsyError(response, 'Etsy shop lookup');
  const body = await response.json();
  return String(body.results?.[0]?.shop_id || body.shop_id || '');
}

async function fetchEvery(url, headers, max = 1000) {
  const results = [];
  for (let offset = 0; offset < max; offset += 100) {
    const pageUrl = new URL(url);
    pageUrl.searchParams.set('limit', '100');
    pageUrl.searchParams.set('offset', String(offset));
    const response = await fetch(pageUrl, { headers });
    if (!response.ok) await etsyError(response, 'Etsy sync');
    const body = await response.json();
    const page = body.results || [];
    results.push(...page);
    if (page.length < 100 || results.length >= Number(body.count || 0)) break;
  }
  return results;
}

async function imageForUpload(imageUrl, dataDir) {
  if (imageUrl.startsWith('/uploads/')) {
    const fileName = path.basename(imageUrl);
    return new Blob([await readFile(path.join(dataDir, 'uploads', fileName))]);
  }
  const safeUrl = await assertSafeRemoteUrl(imageUrl);
  const response = await fetch(safeUrl, { redirect: 'error', signal: AbortSignal.timeout(15_000) });
  if (!response.ok) throw new Error(`Could not load product image (${response.status})`);
  const length = Number(response.headers.get('content-length') || 0);
  if (length > 10 * 1024 * 1024) throw new Error('Product image exceeds 10 MB');
  return response.blob();
}

export function createEtsyActions({ store, key, config, dataDir }) {
  return {
    async etsyAuth(body) {
      requireConfig(config);
      if (body.action === 'status') {
        const token = decryptJson(key, store.getSecret('etsy'));
        return token ? { connected: true, shop_id: token.shop_id, expired: new Date(token.expires_at) < new Date(), scopes: token.scopes } : { connected: false };
      }

      if (body.action === 'get_auth_url') {
        const redirectUri = allowedRedirect(body.redirect_uri, config);
        const verifier = base64url(randomBytes(48));
        const challenge = createHash('sha256').update(verifier).digest('base64url');
        const state = base64url(randomBytes(32));
        await store.setSetting('etsy_oauth_pending', { state, verifier, redirect_uri: redirectUri, expires_at: Date.now() + 10 * 60_000 });
        const params = new URLSearchParams({
          response_type: 'code', client_id: config.etsyApiKey, redirect_uri: redirectUri,
          scope: 'listings_r listings_w transactions_r shops_r email_r', state,
          code_challenge: challenge, code_challenge_method: 'S256',
        });
        return { auth_url: `https://www.etsy.com/oauth/connect?${params}` };
      }

      if (body.action === 'exchange') {
        const pending = store.getSetting('etsy_oauth_pending');
        if (!pending || pending.expires_at < Date.now() || pending.state !== body.state) throw new Error('Invalid or expired Etsy authorization state');
        if (pending.redirect_uri !== body.redirect_uri) throw new Error('Etsy redirect URI mismatch');
        await store.deleteSetting('etsy_oauth_pending');
        const response = await fetch(`${ETSY_ROOT}/public/oauth/token`, {
          method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
          body: new URLSearchParams({ grant_type: 'authorization_code', client_id: config.etsyApiKey, redirect_uri: pending.redirect_uri, code: body.code, code_verifier: pending.verifier }),
        });
        if (!response.ok) await etsyError(response, 'Etsy token exchange');
        const received = await response.json();
        const shopId = config.etsyShopId || await detectShopId(received.access_token, config);
        const token = {
          access_token: received.access_token, refresh_token: received.refresh_token,
          expires_at: new Date(Date.now() + (received.expires_in || 3600) * 1000).toISOString(),
          scopes: received.scope || '', shop_id: String(shopId),
        };
        await saveToken(store, key, token);
        await store.audit('etsy.connect', 'success', { shop_id: token.shop_id });
        return { success: true, shop_id: token.shop_id };
      }

      if (body.action === 'repair') {
        const token = await validToken(store, key, config);
        token.shop_id = config.etsyShopId || await detectShopId(token.access_token, config);
        await saveToken(store, key, token);
        return { success: true, shop_id: token.shop_id };
      }

      if (body.action === 'disconnect') {
        if (body.approved !== true) throw new Error('Ray approval is required to disconnect Etsy');
        await store.recordApproval('etsy.disconnect', { shop_id: (decryptJson(key, store.getSecret('etsy')) || {}).shop_id });
        await store.deleteSecret('etsy');
        await store.audit('etsy.disconnect', 'success');
        return { success: true };
      }
      throw new Error(`Unknown Etsy action: ${body.action}`);
    },

    async etsySyncListings() {
      const token = await validToken(store, key, config);
      const url = `${ETSY_ROOT}/application/shops/${token.shop_id}/listings?state=active&includes=Images`;
      const listings = await fetchEvery(url, apiHeaders(config, token.access_token));
      const existing = store.list('Product');
      let created = 0; let updated = 0;
      for (const listing of listings) {
        const listingUrl = listing.url || `https://www.etsy.com/listing/${listing.listing_id}`;
        const images = (listing.images || []).sort((a, b) => (a.rank || 0) - (b.rank || 0)).map(image => image.url_fullxfull).filter(Boolean);
        const value = {
          title: listing.title || 'Untitled', price: parseMoney(listing.price), etsy_price: parseMoney(listing.price),
          etsy_url: listingUrl, etsy_listing_id: String(listing.listing_id), etsy_title: listing.title || '',
          etsy_description: listing.description || '', etsy_tags: (listing.tags || []).join(', '),
          image_url: images[0] || '', images, tagline: listing.title || '',
        };
        const match = existing.find(product => product.etsy_listing_id === String(listing.listing_id) || product.etsy_url === listingUrl);
        if (match) { await store.update('Product', match.id, value); updated += 1; }
        else { await store.create('Product', value); created += 1; }
      }
      await store.audit('etsy.sync_listings', 'success', { total: listings.length, created, updated });
      return { success: true, total: listings.length, created, updated };
    },

    async etsySyncOrders() {
      const token = await validToken(store, key, config);
      const receipts = await fetchEvery(`${ETSY_ROOT}/application/shops/${token.shop_id}/receipts?includes=Transactions`, apiHeaders(config, token.access_token));
      const existing = store.list('Order');
      let created = 0; let updated = 0;
      for (const receipt of receipts) {
        const number = `ETSY-${receipt.receipt_id}`;
        const transactions = receipt.transactions || [];
        const first = transactions[0] || {};
        const value = {
          order_number: number, marketplace: 'Etsy',
          status: receipt.was_canceled ? 'Cancelled' : receipt.was_shipped ? 'Shipped' : 'Pending',
          customer_name: receipt.name || 'Unknown', customer_email: receipt.buyer_email || '',
          customer_postcode: receipt.zip || '', product_title: first.title || `${transactions.length} item(s)`,
          quantity: transactions.reduce((sum, item) => sum + (item.quantity || 1), 0) || 1,
          sale_price: parseMoney(first.price), shipping_cost: parseMoney(receipt.total_shipping_cost), total: parseMoney(receipt.grandtotal),
          order_date: receipt.creation_tsz ? new Date(receipt.creation_tsz * 1000).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10),
          marketplace_order_url: `https://www.etsy.com/your/shops/${token.shop_id}/orders/${receipt.receipt_id}`,
          notes: receipt.message_from_buyer || '',
        };
        const match = existing.find(order => order.order_number === number);
        if (match) { await store.update('Order', match.id, value); updated += 1; }
        else { await store.create('Order', value); created += 1; }
      }
      await store.audit('etsy.sync_orders', 'success', { total: receipts.length, created, updated });
      return { success: true, total: receipts.length, created, updated };
    },

    async etsyPushListing(body) {
      if (body.approved !== true) throw new Error('Ray approval is required before creating an Etsy draft');
      const product = store.find('Product', body.product_id);
      if (!product) throw new Error('Product not found');
      if (product.etsy_url) throw new Error('This product already has an Etsy listing URL');
      const taxonomyId = Number(body.taxonomy_id || product.etsy_taxonomy_id);
      if (!taxonomyId) throw new Error('Choose a valid Etsy taxonomy/category before creating the draft');
      const price = Number(product.etsy_price || product.price);
      if (!(price > 0)) throw new Error('Set a valid Etsy price before creating the draft');
      const token = await validToken(store, key, config);
      const listing = {
        quantity: Math.max(1, Math.min(999, Number(body.quantity || product.quantity || 1))),
        title: String(product.etsy_title || product.title || '').slice(0, 140),
        description: String(product.etsy_description || product.tagline || '').slice(0, 5000),
        price, who_made: 'i_did', when_made: 'made_to_order', taxonomy_id: taxonomyId,
        tags: String(product.etsy_tags || '').split(',').map(tag => tag.trim()).filter(tag => tag && tag.length <= 20).slice(0, 13),
        state: 'draft',
        ...((body.shipping_profile_id || config.etsyShippingProfileId) ? { shipping_profile_id: Number(body.shipping_profile_id || config.etsyShippingProfileId) } : {}),
        ...((body.readiness_state_id || config.etsyReadinessStateId) ? { readiness_state_id: Number(body.readiness_state_id || config.etsyReadinessStateId) } : {}),
      };
      await store.recordApproval('etsy.create_draft', { product_id: product.id, title: listing.title, price, quantity: listing.quantity });
      const response = await fetch(`${ETSY_ROOT}/application/shops/${token.shop_id}/listings`, {
        method: 'POST', headers: apiHeaders(config, token.access_token, true), body: JSON.stringify(listing),
      });
      if (!response.ok) await etsyError(response, 'Etsy draft creation');
      const created = await response.json();
      const listingUrl = created.url || `https://www.etsy.com/listing/${created.listing_id}`;
      await store.update('Product', product.id, { etsy_url: listingUrl, etsy_listing_id: String(created.listing_id) });
      if (product.image_url) {
        try {
          const form = new FormData();
          form.append('image', await imageForUpload(product.image_url, dataDir), 'product-image');
          form.append('rank', '1');
          const imageResponse = await fetch(`${ETSY_ROOT}/application/shops/${token.shop_id}/listings/${created.listing_id}/images`, {
            method: 'POST', headers: apiHeaders(config, token.access_token), body: form,
          });
          if (!imageResponse.ok) await etsyError(imageResponse, 'Etsy image upload');
        } catch (error) {
          await store.audit('etsy.upload_image', 'failed', { listing_id: created.listing_id, error: error.message });
        }
      }
      await store.audit('etsy.create_draft', 'success', { product_id: product.id, listing_id: created.listing_id });
      return { success: true, listing_url: listingUrl, listing_id: created.listing_id, state: 'draft' };
    },
  };
}
