"""
Project:    Shuffle & Skirmish Market Scraper
Author:     Edward Middleton-Smith
            Shuffle & Skirmish

Technology: Business Objects
Feature:    Cost Fetcher Class
"""

# Internal
from product_scraper import Product_Scraper
from card_market_product_scraper import Card_Market_Product_Scraper
from chaos_cards_product_scraper import Chaos_Cards_Product_Scraper
from games_lore_product_scraper import Games_Lore_Product_Scraper
from magic_madhouse_product_scraper import Magic_Madhouse_Product_Scraper
from the_game_collection_product_scraper import The_Game_Collection_Product_Scraper
# from new_realities_gaming_product_scraper import New_Realities_Gaming_Product_Scraper
from tcg_sole_trader_workbook_container import TCG_Sole_Trader_Workbook_Container

# External
import pandas as pd
from openpyxl import load_workbook, Workbook
from openpyxl.worksheet.worksheet import Worksheet
import requests
import re
import time
import random
from playwright.sync_api import sync_playwright, Browser, Page
from playwright.async_api import async_playwright
import asyncio
from aioconsole import ainput
from collections import defaultdict
from datetime import datetime, timedelta

class Product_Scraper_Controller:
    ACCESSED_LAST_ON_FLAG: str = 'Accessed Last On'
    ACTIVE_FLAG: str = 'Active'
    COST_FLAG: str = 'Cost'
    DATA_FLAG: str = 'Data'
    ERROR_FLAG: str = 'Error'
    INDEX_DOMAIN_FLAG: str = 'Index Domain'
    INDEX_ROW_FLAG: str = 'Index Row'
    NAME_FLAG: str = 'Name'
    PRICE_FLAG: str = 'Price'
    SUCCESS_FLAG: str = 'Success'
    URL_FLAG: str = 'Url'

    domain_names: list[str]
    eur_to_gbp_rate: float
    product_scrapers: list[Product_Scraper]
    product_sources: pd.DataFrame
    workbook_container: TCG_Sole_Trader_Workbook_Container

    def __init__(self):
        self.domain_names = [
              Card_Market_Product_Scraper.NAME_DOMAIN_CARD_MARKET
            , Chaos_Cards_Product_Scraper.NAME_DOMAIN_CHAOS_CARDS
            , Games_Lore_Product_Scraper.NAME_DOMAIN_GAMES_LORE
            , Magic_Madhouse_Product_Scraper.NAME_DOMAIN_MAGIC_MADHOUSE
            , The_Game_Collection_Product_Scraper.NAME_DOMAIN_THE_GAME_COLLECTION
        ]
        domain_details = {}
        product_scrapers = []
        for index_domain in range(len(self.domain_names)):
            domain_name = self.domain_names[index_domain]
            domain_details[domain_name] = {
                  self.NAME_FLAG: domain_name
                , self.INDEX_DOMAIN_FLAG: self.get_index_domain_from_name(domain_name)
                , self.ACCESSED_LAST_ON_FLAG: 0
            }
            # product_scraper = Product_Scraper(domain)
            product_scraper = None
            if domain_name == Card_Market_Product_Scraper.NAME_DOMAIN_CARD_MARKET:
                product_scraper = Card_Market_Product_Scraper()
            elif domain_name == Chaos_Cards_Product_Scraper.NAME_DOMAIN_CHAOS_CARDS:
                product_scraper = Chaos_Cards_Product_Scraper()
            elif domain_name == Games_Lore_Product_Scraper.NAME_DOMAIN_GAMES_LORE:
                product_scraper = Games_Lore_Product_Scraper()
            elif domain_name == Magic_Madhouse_Product_Scraper.NAME_DOMAIN_MAGIC_MADHOUSE:
                product_scraper = Magic_Madhouse_Product_Scraper()
            elif domain_name == The_Game_Collection_Product_Scraper.NAME_DOMAIN_THE_GAME_COLLECTION:
                product_scraper = The_Game_Collection_Product_Scraper()
            
            if product_scraper is not None:
                product_scrapers.append(product_scraper)
            else:
                raise ValueError(f'Domain not known: {index_domain}, {domain_name}')
        self.domain_details = domain_details
        self.product_scrapers = product_scrapers
        self.workbook_container = TCG_Sole_Trader_Workbook_Container()
        self.get_eur_to_gbp_rate()
    
    async def fetch_single_with_browser(self, browser, domain_name, product_source):
        """Fetch a single URL using the provided browser"""
        index_row = product_source[TCG_Sole_Trader_Workbook_Container.NAME_COLUMN_INDEX_ROW]
        source_link = product_source[TCG_Sole_Trader_Workbook_Container.NAME_COLUMN_LINK]
        print(f"\n\nFetching\nrow: {index_row}\ndomain: {domain_name}\nlink: {source_link}")
        
        self.workbook_container.clear_row_sourcing_sheet(index_row = index_row)
        self.log_processing_new_row(
            index_row = index_row
            , source_link = source_link
        )
        
        index_domain = self.get_index_domain_from_name(domain_name)
        cost = None
        price = None
        active = None
        
        try:
            if domain_name == Card_Market_Product_Scraper.NAME_DOMAIN_CARD_MARKET:
                if product_source[TCG_Sole_Trader_Workbook_Container.NAME_COLUMN_PRODUCT_IS_BOOSTER]:
                    price_quantity_pairs = await self.product_scrapers[index_domain].scrape_prices_and_quantities_playwright(
                          browser = browser
                        , url = source_link
                        , eur_to_gbp_rate = self.eur_to_gbp_rate
                    )
                    price = self.get_sale_price_from_price_quantity_pairs(price_quantity_pairs = price_quantity_pairs)
                cost, active = await self.product_scrapers[index_domain].scrape_cost_and_active_playwright(
                      browser = browser
                    , url = source_link
                    , eur_to_gbp_rate = self.eur_to_gbp_rate
                )
            elif domain_name in (
                  Chaos_Cards_Product_Scraper.NAME_DOMAIN_CHAOS_CARDS
                , Games_Lore_Product_Scraper.NAME_DOMAIN_GAMES_LORE
                , Magic_Madhouse_Product_Scraper.NAME_DOMAIN_MAGIC_MADHOUSE
                , The_Game_Collection_Product_Scraper.NAME_DOMAIN_THE_GAME_COLLECTION
            ):
                cost, active = await self.product_scrapers[index_domain].scrape_cost_and_active_playwright(
                      browser = browser
                    , url = source_link
                )
            """ unverified
            elif domain_name == self.NAME_DOMAIN_NEW_REALITIES_GAMING:
                cost, active = await self.product_scrapers[index_domain].scrape_cost_and_active_playwright(
                      browser = browser
                    , url = source_link
                )
            """
            if ((cost is None and price is None) or active is None):
                print(f"\n\nError: Could not find cost on page\nrow: {index_row}\ndomain: {domain_name}\nlink: {source_link}\nprice: {price}\ncost: {cost}\nactive: {active}")
                
        except Exception as e:
            print(f"\n\nError: Could not find cost on page\nrow: {index_row}\ndomain: {domain_name}\nlink: {source_link}\nprice: {price}\ncost: {cost}\nactive: {active}\nerror: {e}")
        
        return self.make_result_data_json(
            index_row = index_row
            , cost = cost
            , price = price
            , active = active
        )
    
    async def fetch_all(self):
        try:
            processed_count = 0
            updated_count = 0
            self.product_sources = self.workbook_container.get_sourcing_entries()
            
            # Group product sources by domain
            domain_groups = {domain: [] for domain in self.domain_names}
            for index_product_source, product_source in self.product_sources.iterrows():
                source_name = product_source[TCG_Sole_Trader_Workbook_Container.NAME_COLUMN_SOURCE_NAME]
                if source_name in domain_groups:
                    domain_groups[source_name].append(product_source)
            
            # Create one browser per domain and process all URLs for that domain
            async with async_playwright() as p:
                # Create one browser per domain that has URLs
                domain_tasks = []
                for domain_name in self.domain_names:
                    if domain_groups[domain_name]:  # Only if there are URLs for this domain
                        browser = await p.chromium.launch(headless=True)
                        task = self.process_domain_urls(browser, domain_name, domain_groups[domain_name])
                        domain_tasks.append(task)
                
                # Process all domains in parallel
                all_sourced_products = await asyncio.gather(*domain_tasks)
                
                # Flatten results from all domains
                sourced_products = []
                for domain_results in all_sourced_products:
                    sourced_products.extend(domain_results)
            
            # Process results
            for sourced_product in sourced_products:
                index_row = sourced_product[self.INDEX_ROW_FLAG]
                unit_cost = sourced_product[self.COST_FLAG]
                unit_price = sourced_product[self.PRICE_FLAG]
                active = sourced_product[self.ACTIVE_FLAG]
                processed_count += 1
                if not active:
                    continue
                updated_count += 1
                self.workbook_container.update_row_sourcing_sheet(
                      index_row = index_row
                    , unit_cost = unit_cost
                    , unit_price = unit_price
                    , active = active
                )
            self.workbook_container.save_workbook()
            print(f"\nComplete!")
            print(f"Processed: {processed_count} entries")
            print(f"Updated: {updated_count} costs")
        except Exception as e:
            print(f"Error: {e}")
    
    async def process_domain_urls(self, browser, domain_name, product_sources):
        """Process all URLs for a single domain sequentially with rate limiting"""
        results = []
        domain_details = self.domain_details[domain_name]
        last_access_time = domain_details[self.ACCESSED_LAST_ON_FLAG]
        try:
            for product_source in product_sources:
                # Rate limiting: wait 60s between requests to same domain
                time_since_last = time.time() - last_access_time
                if time_since_last < 45:
                    wait_time = 45 - time_since_last + random.uniform(0, 5)  # 45-50s
                    print(f"  [{domain_name}] Waiting {wait_time:.1f}s before next request...")
                    await asyncio.sleep(wait_time)
                
                # Process the URL
                result = await self.fetch_single_with_browser(browser, domain_name, product_source)
                results.append(result)
                last_access_time = time.time()
                
        finally:
            await browser.close()
            
        return results
    
    def get_index_domain_from_name(self, domain_name):
        for index_domain in range(len(self.domain_names)):
            if (self.domain_names[index_domain] == domain_name):
                return index_domain
        raise ValueError(f'Domain does not exist: {domain_name}')
    
    def get_eur_to_gbp_rate(self):
        try:
            response = requests.get('https://api.exchangerate-api.com/v4/latest/EUR', timeout=10)
            data = response.json()
            self.eur_to_gbp_rate = data['rates']['GBP']
        except Exception as e:
            print(f"Error fetching exchange rate: {e}")
            print("Using fallback rate: 0.85")
            self.eur_to_gbp_rate = 0.85
    
    @classmethod
    def make_result_data_json(cls, index_row, cost = None, price = None, active = None):
        return {
            cls.INDEX_ROW_FLAG: index_row
            , cls.COST_FLAG: cost
            , cls.PRICE_FLAG: price
            , cls.ACTIVE_FLAG: active
        }
    
    def get_sale_price_from_price_quantity_pairs(self, price_quantity_pairs):
        if not price_quantity_pairs: 
            return None
        
        max_quantity = 0
        price = None
        
        # First pass: look for quantity >= 8
        for price_quantity_pair in price_quantity_pairs:
            price = price_quantity_pair['price']
            quantity = price_quantity_pair['quantity']
            # print(f"  Found price: €{eur_price}")
            # print(f"  Found quantity: {quantity}")
            max_quantity = max(max_quantity, quantity)
            
            if quantity >= 8 and price: # eur_price:
                # price = eur_price * self.eur_to_gbp_rate
                # print(f"  Converted: {quantity}x €{eur_price:.2f} → £{price:.2f}")
                return price
        
        # Second pass: use max quantity if no quantity >= 8
        # print("Offer with quantity >= 8 not found")
        for price_quantity_pair in price_quantity_pairs:
            price = price_quantity_pair['price']
            quantity = price_quantity_pair['quantity']
            
            if (max_quantity <= 2 or quantity == max_quantity) and price: # eur_price:
                # price = eur_price * self.eur_to_gbp_rate
                # print(f"  Converted: {quantity}x €{eur_price:.2f} → £{price:.2f}")
                return price
        
        return price
    
    @staticmethod
    def log_processing_new_row(index_row, source_link):
        """
        print(f"\n{'='*60}")
        print(f"Processing row {index_row}: {source_link}")
        print(f"{'='*60}")
        """



async def main():
    controller = Product_Scraper_Controller()
    await controller.fetch_all()

if __name__ == "__main__":
    asyncio.run(main())