const fs = require('fs');
const path = require('path');
const readline = require('readline');
const axios = require('axios');
const cheerio = require('cheerio');
const { PrismaClient } = require('@prisma/client');
const { parseName, sleep } = require('./utils');

const prisma = new PrismaClient();

// Scraper state
const state = {
  isRunning: false,
  concurrency: 5,
  delayMs: 500,
  totalUrls: 0,
  completedCount: 0,
  failedCount: 0,
  pendingCount: 0,
  logs: [],
  activeWorkers: 0
};

// Log helper
function logMsg(message, type = 'info') {
  const logEntry = {
    timestamp: new Date().toISOString(),
    message,
    type // info, success, warning, error
  };
  state.logs.push(logEntry);
  if (state.logs.length > 300) {
    state.logs.shift(); // Keep last 300 logs
  }
  console.log(`[${type.toUpperCase()}] ${message}`);
}

/**
 * Initializes the database by importing URLs from adresler.html if the queue is empty.
 */
async function initializeQueue() {
  try {
    const count = await prisma.scrapeQueue.count();
    logMsg(`Current database queue has ${count} items. Syncing with entries in adresler.html...`, 'info');


    logMsg('Initializing queue from adresler.html... This may take a moment.', 'info');
    const filePath = path.join(__dirname, '../adresler.html');
    
    if (!fs.existsSync(filePath)) {
      logMsg('adresler.html file not found in the root directory!', 'error');
      return;
    }

    const fileStream = fs.createReadStream(filePath);
    const rl = readline.createInterface({
      input: fileStream,
      crlfDelay: Infinity
    });

    const urlRegex = /href="(https:\/\/barkodist\.com\/[^"]+)"/;
    let urls = [];
    let lineCount = 0;
    let importedCount = 0;

    for await (const line of rl) {
      lineCount++;
      const match = line.match(urlRegex);
      if (match) {
        const url = match[1];
        urls.push({ url, status: 'pending' });

        // Insert in batches of 5000 for efficiency
        if (urls.length >= 5000) {
          await prisma.scrapeQueue.createMany({
            data: urls,
            skipDuplicates: true
          });
          importedCount += urls.length;
          logMsg(`Imported ${importedCount} URLs...`, 'info');
          urls = [];
        }
      }
    }

    // Insert remaining URLs
    if (urls.length > 0) {
      await prisma.scrapeQueue.createMany({
        data: urls,
        skipDuplicates: true
      });
      importedCount += urls.length;
    }

    logMsg(`Queue initialization complete. Total imported: ${importedCount} URLs.`, 'success');
    state.totalUrls = importedCount;
    await updateCounts();
  } catch (error) {
    logMsg(`Queue initialization failed: ${error.message}`, 'error');
  }
}

/**
 * Updates completed, failed, and pending counts in state.
 */
async function updateCounts() {
  try {
    state.totalUrls = await prisma.scrapeQueue.count();
    state.completedCount = await prisma.scrapeQueue.count({ where: { status: 'completed' } });
    state.failedCount = await prisma.scrapeQueue.count({ where: { status: 'failed' } });
    state.pendingCount = await prisma.scrapeQueue.count({ where: { status: 'pending' } });
  } catch (error) {
    console.error('Failed to update counts:', error);
  }
}

/**
 * Scrapes a single product URL.
 */
async function scrapeUrl(queueItem) {
  const { id, url } = queueItem;
  
  try {
    // Fetch page content
    const response = await axios.get(url, {
      timeout: 10000,
      headers: {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
        'Accept-Language': 'tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7'
      }
    });

    const html = response.data;
    const $ = cheerio.load(html);

    // 1. Barcode (very critical)
    let barcode = null;
    
    // Attempt 1: From JS script JsBarcode call
    const jsBarcodeMatch = html.match(/JsBarcode\("#barcodeProduct",\s*"(\d+)"\)/);
    if (jsBarcodeMatch && jsBarcodeMatch[1]) {
      barcode = jsBarcodeMatch[1].trim();
    }
    
    // Attempt 2: From hidden inputs (HataBildirModal form)
    if (!barcode) {
      const hiddenBarcodeMatch = html.match(/name="HataBarkod"\s+value="(\d+)"/) || html.match(/value="(\d+)"\s+name="HataBarkod"/);
      if (hiddenBarcodeMatch && hiddenBarcodeMatch[1]) {
        barcode = hiddenBarcodeMatch[1].trim();
      }
    }
    
    // Attempt 3: From paragraph strong tags containing barcode numbers
    if (!barcode) {
      const pText = $('.BarcodeGeneratorInfo p').text() || $('p').text();
      const textBarcodeMatch = pText.match(/barkod numarası\s*<strong>(\d+)<\/strong>/i) || pText.match(/barkod numarası\s+(\d+)/i);
      if (textBarcodeMatch && textBarcodeMatch[1]) {
        barcode = textBarcodeMatch[1].trim();
      }
    }

    if (!barcode) {
      throw new Error('Barcode number could not be found on the page.');
    }

    // 2. Product Name
    const rawName = $('h1.barkodTitle').text().trim() || $('title').text().replace(' | Barkodist', '').replace(' Barkodu', '').trim();
    if (!rawName) {
      throw new Error('Product name could not be found.');
    }

    // Parse clean name, amount, unit
    const parsed = parseName(rawName);

    // 3. Product Image
    let image = null;
    const imgEl = $('.BarkodPageContainer img').first();
    if (imgEl.length) {
      const src = imgEl.attr('src');
      if (src) {
        // Clean double slashes e.g. barkodist.com//assets -> barkodist.com/assets
        image = src.replace(/([^:]\/)\/+/g, "$1");
        if (!image.startsWith('http')) {
          image = 'https://barkodist.com' + (image.startsWith('/') ? '' : '/') + image;
        }
      }
    }

    // 4. Category and Subcategory
    let category = null;
    let subcategory = null;
    
    // From page description paragraph
    // Example: "Gıda Ürünleri kategorisi ve Makarna alt kategorisinde yer alan..."
    const infoText = $('.BarcodeGeneratorInfo').text() || $('body').text();
    const catMatch = infoText.match(/([a-zA-ZçıöşğüÇİÖŞĞÜ\s]+)\s+kategorisi\s+ve\s+([a-zA-ZçıöşğüÇİÖŞĞÜ\s]+)\s+alt\s+kategorisinde/i);
    if (catMatch) {
      category = catMatch[1].trim();
      subcategory = catMatch[2].trim();
    }

    // 5. Company (Owner Company)
    let company = null;
    const companyMatch = html.match(/<b>(.*?)<\/b>\s*şirketine aittir/i) || html.match(/<b>(.*?)<\/b>\s*firmasına aittir/i);
    if (companyMatch && companyMatch[1]) {
      company = companyMatch[1].trim();
    }

    // Save product to database
    await prisma.product.upsert({
      where: { barcode },
      update: {
        name: rawName,
        cleanName: parsed.cleanName,
        amountRaw: parsed.amountRaw,
        amount: parsed.amount,
        unit: parsed.unit,
        image,
        category,
        subcategory,
        company,
        url,
        updatedAt: new Date()
      },
      create: {
        barcode,
        name: rawName,
        cleanName: parsed.cleanName,
        amountRaw: parsed.amountRaw,
        amount: parsed.amount,
        unit: parsed.unit,
        image,
        category,
        subcategory,
        company,
        url
      }
    });

    // Update queue status
    await prisma.scrapeQueue.update({
      where: { id },
      data: {
        status: 'completed',
        error: null
      }
    });

    logMsg(`Successfully scraped: "${parsed.cleanName}" (${barcode})`, 'success');
  } catch (error) {
    const errorMsg = error.response ? `HTTP ${error.response.status}` : error.message;
    logMsg(`Failed to scrape ${url}: ${errorMsg}`, 'error');
    
    await prisma.scrapeQueue.update({
      where: { id },
      data: {
        status: 'failed',
        error: errorMsg,
        retryCount: { increment: 1 }
      }
    });
  }
}

// Central queue manager in memory to avoid MySQL deadlock/transaction errors
let queueBuffer = [];
let isFetchingQueue = false;

async function getNextQueueItem() {
  if (queueBuffer.length > 0) {
    return queueBuffer.shift();
  }
  
  if (isFetchingQueue) {
    await sleep(100);
    return getNextQueueItem();
  }
  
  isFetchingQueue = true;
  try {
    const items = await prisma.scrapeQueue.findMany({
      where: { status: 'pending' },
      orderBy: { id: 'asc' },
      take: 20
    });
    
    if (items.length === 0) {
      isFetchingQueue = false;
      return null;
    }
    
    const ids = items.map(item => item.id);
    await prisma.scrapeQueue.updateMany({
      where: { id: { in: ids } },
      data: { status: 'processing' }
    });
    
    queueBuffer = items;
  } catch (error) {
    console.error('Error fetching queue batch:', error);
  } finally {
    isFetchingQueue = false;
  }
  
  return queueBuffer.shift() || null;
}

async function resetQueueBuffer() {
  if (queueBuffer.length > 0) {
    try {
      const ids = queueBuffer.map(item => item.id);
      queueBuffer = [];
      await prisma.scrapeQueue.updateMany({
        where: { id: { in: ids } },
        data: { status: 'pending' }
      });
      console.log(`Reset ${ids.length} buffered items back to pending.`);
    } catch (error) {
      console.error('Error resetting queue buffer:', error);
    }
  }
}

/**
 * Worker loop that grabs a pending item and scrapes it.
 */
async function scraperWorker() {
  state.activeWorkers++;
  while (state.isRunning) {
    let queueItem = null;
    
    try {
      queueItem = await getNextQueueItem();

      if (!queueItem) {
        // No more pending items
        break;
      }

      await scrapeUrl(queueItem);
      await updateCounts();
      
      // Be nice: wait after scraping
      if (state.delayMs > 0) {
        await sleep(state.delayMs);
      }
    } catch (error) {
      console.error('Worker error:', error);
      if (queueItem) {
        // Reset status to pending so it can be retried
        await prisma.scrapeQueue.update({
          where: { id: queueItem.id },
          data: { status: 'pending' }
        }).catch(() => {});
      }
      await sleep(2000);
    }
  }
  state.activeWorkers--;
  
  if (state.activeWorkers === 0 && state.isRunning) {
    state.isRunning = false;
    logMsg('Scraping job completed or queue empty.', 'success');
  }
}

/**
 * Starts scraping with the specified settings.
 */
async function startScraping(concurrency = 5, delayMs = 500) {
  if (state.isRunning) {
    logMsg('Scraper is already running.', 'warning');
    return;
  }

  state.isRunning = true;
  state.concurrency = concurrency;
  state.delayMs = delayMs;
  
  logMsg(`Starting scraper with concurrency: ${concurrency}, delay: ${delayMs}ms`, 'info');
  await updateCounts();

  // Spawn workers
  for (let i = 0; i < concurrency; i++) {
    scraperWorker();
  }
}

/**
 * Pauses the scraper.
 */
async function pauseScraping() {
  if (!state.isRunning) {
    logMsg('Scraper is not running.', 'warning');
    return;
  }
  
  state.isRunning = false;
  logMsg('Pausing scraper... Active workers will finish their current URL.', 'warning');
  await resetQueueBuffer();
}


/**
 * Resets failed items in the queue back to pending to retry them.
 */
async function retryFailed() {
  try {
    const result = await prisma.scrapeQueue.updateMany({
      where: { status: 'failed' },
      data: { status: 'pending' }
    });
    logMsg(`Reset ${result.count} failed URLs to pending status.`, 'success');
    await updateCounts();
    return result.count;
  } catch (error) {
    logMsg(`Failed to reset retry queue: ${error.message}`, 'error');
  }
}

/**
 * Returns current scraping statistics and latest logs.
 */
function getStats() {
  return {
    isRunning: state.isRunning,
    activeWorkers: state.activeWorkers,
    concurrency: state.concurrency,
    delayMs: state.delayMs,
    totalUrls: state.totalUrls,
    completedCount: state.completedCount,
    failedCount: state.failedCount,
    pendingCount: state.pendingCount,
    logs: state.logs
  };
}

module.exports = {
  initializeQueue,
  startScraping,
  pauseScraping,
  retryFailed,
  getStats,
  updateCounts
};
