import asyncio
import aiohttp
from aiohttp import ClientSession, TCPConnector, ClientTimeout
import nltk
from nltk.sentiment.vader import SentimentIntensityAnalyzer
from newspaper import Article, ArticleException
from bs4 import BeautifulSoup
import re
import os
import logging
from colorama import init, Fore, Style
import gzip
import pickle
from typing import List, Optional, Tuple
from dataclasses import dataclass
import time
from urllib.parse import urlparse
from enum import Enum
import argparse
from tqdm import tqdm
import aiofiles

# Inițializare Colorama și Logging
init(autoreset=True)
logging.basicConfig(filename='processing.log', level=logging.DEBUG, 
                    format='%(asctime)s - %(levelname)s - %(message)s')

# Inițializarea analizatorului de sentiment VADER
nltk.download('vader_lexicon', quiet=True)
sid = SentimentIntensityAnalyzer()

class SearchMode(Enum):
    OR = "sau"
    AND = "și"

@dataclass
class ProcessedLink:
    url: str
    content: str
    sentiment_score: float

class WebCrawler:
    def __init__(self, keywords: List[str], sentiment_threshold: float, search_mode: SearchMode, max_concurrent_requests: int = 10):
        self.keywords = keywords
        self.sentiment_threshold = sentiment_threshold
        self.search_mode = search_mode
        self.session: Optional[ClientSession] = None
        self.rate_limit = asyncio.Semaphore(max_concurrent_requests)
        self.timeout = ClientTimeout(total=30)

    async def download_file(self, url: str, local_filename: str) -> bool:
        print(f"{Fore.BLUE}Se descarcă fișierul de la: {url}{Style.RESET_ALL}")
        try:
            async with self.rate_limit:
                async with self.session.get(url, timeout=self.timeout) as response:
                    response.raise_for_status()
                    async with aiofiles.open(local_filename, 'wb') as f:
                        await f.write(await response.read())
            print(f"{Fore.GREEN}Fișierul a fost salvat ca {local_filename}{Style.RESET_ALL}")
            logging.info(f"Fișierul a fost salvat ca {local_filename}")
            return True
        except Exception as e:
            print(f"{Fore.RED}Eroare la descărcarea fișierului {url}: {e}{Style.RESET_ALL}")
            logging.error(f"Eroare la descărcarea fișierului {url}: {e}")
            return False

    @staticmethod
    async def save_extracted_text(link: str, text: str) -> None:
        result_filename = f"extracted_text_{urlparse(link).netloc}_{int(time.time())}.txt"
        result_folder = "extracted_texts"
        os.makedirs(result_folder, exist_ok=True)
        result_path = os.path.join(result_folder, result_filename)
        try:
            async with aiofiles.open(result_path, 'w', encoding='utf-8') as f:
                await f.write(f"Link: {link}\n\nText extras:\n{text}\n")
            print(f"{Fore.GREEN}Textul extras a fost salvat în {result_path}{Style.RESET_ALL}")
            logging.info(f"Textul extras a fost salvat în {result_path}")
        except Exception as e:
            print(f"{Fore.RED}Eroare la salvarea textului extras în {result_path}: {e}{Style.RESET_ALL}")
            logging.error(f"Eroare la salvarea textului extras în {result_path}: {e}")

    async def extract_text_from_link(self, link: str) -> str:
        try:
            article = Article(link)
            await asyncio.to_thread(article.download)
            await asyncio.to_thread(article.parse)
            return article.text
        except ArticleException as e:
            logging.warning(f"ArticleException la procesarea linkului {link}: {e}")
            return ""
        except Exception as e:
            logging.error(f"Eroare neașteptată la procesarea linkului {link}: {e}")
            return ""

    @staticmethod
    def analyze_sentiment(text: str) -> float:
        sentiment = sid.polarity_scores(text)
        return sentiment['neg']

    def contains_keywords(self, text: str) -> bool:
        if self.search_mode == SearchMode.OR:
            return any(keyword.lower() in text.lower() for keyword in self.keywords)
        elif self.search_mode == SearchMode.AND:
            return all(keyword.lower() in text.lower() for keyword in self.keywords)

    async def process_link(self, link: str) -> Optional[ProcessedLink]:
        max_retries = 3
        for attempt in range(max_retries):
            try:
                print(f"{Fore.CYAN}Procesare link: {link} (încercarea {attempt + 1}/{max_retries}){Style.RESET_ALL}")
                async with self.rate_limit:
                    content = await self.extract_text_from_link(link)
                if content and len(content) >= 700:
                    if self.contains_keywords(content):
                        negative_sentiment = self.analyze_sentiment(content)
                        print(f"{Fore.CYAN}Scor sentiment negativ pentru linkul {link}: {negative_sentiment}{Style.RESET_ALL}")
                        
                        text_preview = content[:100].replace('\n', ' ')
                        print(f"{Fore.YELLOW}Preview text: {text_preview}...{Style.RESET_ALL}")

                        if negative_sentiment > self.sentiment_threshold:
                            print(f"{Fore.YELLOW}Sentiment negativ ridicat detectat în linkul: {link}{Style.RESET_ALL}")
                            await self.save_extracted_text(link, content)
                            return ProcessedLink(link, content, negative_sentiment)
                        else:
                            print(f"{Fore.GREEN}Nu s-a detectat un sentiment negativ ridicat în linkul: {link}{Style.RESET_ALL}")
                    else:
                        print(f"{Fore.YELLOW}Textul de la linkul {link} nu conține cuvintele cheie căutate.{Style.RESET_ALL}")
                else:
                    print(f"{Fore.YELLOW}Textul de la linkul {link} este insuficient pentru analiză.{Style.RESET_ALL}")
                return None
            except Exception as e:
                print(f"{Fore.RED}Eroare la procesarea linkului {link} (încercarea {attempt + 1}/{max_retries}): {e}{Style.RESET_ALL}")
                logging.error(f"Eroare la procesarea linkului {link} (încercarea {attempt + 1}/{max_retries}): {e}")
                if attempt == max_retries - 1:
                    print(f"{Fore.RED}Abandonarea procesării linkului {link} după {max_retries} încercări{Style.RESET_ALL}")
                else:
                    await asyncio.sleep(2 ** attempt)  # Exponential backoff
        return None

    async def process_links(self, links: List[str]) -> List[ProcessedLink]:
        print(f"{Fore.BLUE}Se procesează {len(links)} linkuri...{Style.RESET_ALL}")
        tasks = [self.process_link(link) for link in links]
        results = await asyncio.gather(*tasks, return_exceptions=True)
        return [result for result in results if isinstance(result, ProcessedLink)]

    async def get_latest_datasets(self) -> List[str]:
        print(f"{Fore.BLUE}Se obțin datele disponibile de la Common Crawl...{Style.RESET_ALL}")
        url = "https://data.commoncrawl.org/crawl-data/index.html"
        try:
            async with self.rate_limit:
                async with self.session.get(url, timeout=self.timeout) as response:
                    response.raise_for_status()
                    content = await response.text()

            soup = BeautifulSoup(content, 'html.parser')
            rows = soup.find_all('tr')
            datasets = [row.find_all('td')[0].text.strip() for row in rows[1:]]
            print(f"{Fore.GREEN}Numărul de seturi de date disponibile: {len(datasets)}{Style.RESET_ALL}")
            logging.info(f"Numărul de seturi de date disponibile: {len(datasets)}")
            return datasets
        except Exception as e:
            print(f"{Fore.RED}Eroare la obținerea seturilor de date: {e}{Style.RESET_ALL}")
            logging.error(f"Eroare la obținerea seturilor de date: {e}")
            return []

    async def get_wet_file_paths(self, dataset_id: str) -> List[str]:
        dataset_url = f"https://data.commoncrawl.org/crawl-data/{dataset_id}/wet.paths.gz"
        local_gz_file = f"{dataset_id}_wet.paths.gz"

        if await self.download_file(dataset_url, local_gz_file):
            try:
                async with aiofiles.open(local_gz_file, 'rb') as f:
                    content = await f.read()
                    with gzip.open(gzip.io.BytesIO(content), 'rt') as gz_file:
                        wet_file_paths = gz_file.read().splitlines()
                os.remove(local_gz_file)
                return wet_file_paths
            except Exception as e:
                print(f"{Fore.RED}Eroare la citirea fișierului {local_gz_file}: {e}{Style.RESET_ALL}")
                logging.error(f"Eroare la citirea fișierului {local_gz_file}: {e}")
                return []
        else:
            return []

    async def download_wet_file(self, wet_file_path: str) -> Optional[str]:
        base_url = "https://data.commoncrawl.org/"
        local_filename = wet_file_path.split('/')[-1]
        full_url = base_url + wet_file_path

        if not os.path.exists(local_filename):
            if await self.download_file(full_url, local_filename):
                return local_filename
        return None

    async def process_wet_file(self, wet_file_path: str) -> List[str]:
        local_wet_file = await self.download_wet_file(wet_file_path)

        if not local_wet_file:
            return []

        try:
            print(f"{Fore.CYAN}Se procesează fișierul WET: {local_wet_file}{Style.RESET_ALL}")

            async with aiofiles.open(local_wet_file, 'rb') as f:
                content = await f.read()
                with gzip.open(gzip.io.BytesIO(content), 'rt', encoding='utf-8') as gz_file:
                    lines = gz_file.readlines()

            all_links = re.findall(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', ' '.join(lines))

            print(f"{Fore.GREEN}Extrase {len(all_links)} linkuri din {local_wet_file}{Style.RESET_ALL}")
            logging.info(f"Extrase {len(all_links)} linkuri din {local_wet_file}")

            os.remove(local_wet_file)

            return all_links

        except Exception as e:
            print(f"{Fore.RED}Eroare la procesarea fișierului {local_wet_file}: {e}{Style.RESET_ALL}")
            logging.error(f"Eroare la procesarea fișierului {local_wet_file}: {e}")
            return []

    @staticmethod
    async def save_progress(dataset_index: int, wet_file_index: int) -> None:
        async with aiofiles.open('progress.pickle', 'wb') as f:
            await f.write(pickle.dumps((dataset_index, wet_file_index)))

    @staticmethod
    async def load_progress() -> Tuple[int, int]:
        if os.path.exists('progress.pickle'):
            async with aiofiles.open('progress.pickle', 'rb') as f:
                content = await f.read()
                return pickle.loads(content)
        else:
            return 0, 0

    async def run(self, resume_progress: bool = False) -> None:
        connector = TCPConnector(limit=100)  # Limitează numărul de conexiuni simultane
        async with aiohttp.ClientSession(connector=connector, timeout=self.timeout) as session:
            self.session = session
            if resume_progress:
                dataset_index, wet_file_index = await self.load_progress()
            else:
                dataset_index, wet_file_index = 0, 0

            datasets = await self.get_latest_datasets()

            try:
                for i in range(dataset_index, len(datasets)):
                    dataset_id = datasets[i]
                    print(f"{Fore.GREEN}Se procesează setul de date: {dataset_id}{Style.RESET_ALL}")

                    wet_file_paths = await self.get_wet_file_paths(dataset_id)

                    for j in tqdm(range(wet_file_index, len(wet_file_paths)), desc="Procesare fișiere WET"):
                        wet_file_path = wet_file_paths[j]
                        all_links = await self.process_wet_file(wet_file_path)

                        if all_links:
                            processed_links = await self.process_links(all_links)
                            print(f"{Fore.BLUE}Procesare completă pentru {wet_file_path}, {len(processed_links)} linkuri au fost extrase și analizate.{Style.RESET_ALL}")

                        await self.save_progress(i, j + 1)

                    wet_file_index = 0
                    await self.save_progress(i + 1, wet_file_index)

            except KeyboardInterrupt:
                print(f"{Fore.RED}Execuția a fost întreruptă de utilizator. Se salvează progresul...{Style.RESET_ALL}")
                await self.save_progress(dataset_index, wet_file_index)

def get_keywords_from_user() -> List[str]:
    while True:
        keywords = input(f"{Fore.CYAN}Introduceți unul sau mai multe cuvinte cheie pentru filtrarea textelor (separate prin virgulă): {Style.RESET_ALL}").strip()
        keyword_list = [k.strip() for k in keywords.split(',') if k.strip()]
        if keyword_list:
            print(f"{Fore.GREEN}Cuvinte cheie introduse: {', '.join(keyword_list)}{Style.RESET_ALL}")
            return keyword_list
        print(f"{Fore.RED}Trebuie să introduceți cel puțin un cuvânt cheie. Încercați din nou.{Style.RESET_ALL}")

def get_search_mode_from_user() -> SearchMode:
    while True:
        mode = input(f"{Fore.CYAN}Alegeți modul de căutare ('sau' pentru oricare cuvânt, 'și' pentru toate cuvintele): {Style.RESET_ALL}").strip().lower()
        if mode in ['sau', 'si', 'și']:
            return SearchMode.OR if mode == 'sau' else SearchMode.AND
        print(f"{Fore.RED}Vă rugăm să alegeți 'sau' sau 'și'.{Style.RESET_ALL}")

def get_sentiment_threshold_from_user() -> float:
    while True:
        try:
            threshold = float(input(f"{Fore.CYAN}Introduceți pragul pentru scorul de sentiment negativ (0.0 - 1.0): {Style.RESET_ALL}"))
            if 0.0 <= threshold <= 1.0:
                return threshold
            print(f"{Fore.RED}Valoarea trebuie să fie între 0.0 și 1.0. Încercați din nou.{Style.RESET_ALL}")
        except ValueError:
            print(f"{Fore.RED}Vă rugăm să introduceți un număr valid.{Style.RESET_ALL}")

def get_resume_choice_from_user() -> bool:
    while True:
        choice = input(f"{Fore.CYAN}S-a găsit un progres salvat anterior. Doriți să reluați execuția de unde a rămas ultima dată? (da/nu): {Style.RESET_ALL}").strip().lower()
        if choice in ['da', 'nu']:
            return choice == 'da'
        print(f"{Fore.RED}Vă rugăm să răspundeți cu 'da' sau 'nu'.{Style.RESET_ALL}")

def print_sentiment_explanation():
    print(f"{Fore.YELLOW}Explicație scor de sentiment:{Style.RESET_ALL}")
    print("Scorul de sentiment negativ variază între 0.0 și 1.0.")
    print("- 0.0 reprezintă absența totală a sentimentului negativ.")
    print("- 1.0 reprezintă un sentiment extrem de negativ.")
    print("Pentru texte normale, fără amenințări, scorurile tipice sunt de obicei sub 0.1.")
    print("Un scor peste 0.3 poate indica un conținut semnificativ negativ sau potențial amenințător.")
    print("Valoarea implicită recomandată pentru prag este 0.107.\n")

def parse_arguments():
    parser = argparse.ArgumentParser(description="Web Crawler cu analiză de sentiment")
    parser.add_argument("-k", "--keywords", nargs="+", help="Cuvinte cheie pentru căutare")
    parser.add_argument("-m", "--mode", choices=["sau", "si"], help="Modul de căutare: 'sau' sau 'si'")
    parser.add_argument("-t", "--threshold", type=float, help="Pragul pentru scorul de sentiment negativ (0.0 - 1.0)")
    parser.add_argument("-r", "--resume", action="store_true", help="Reia execuția de unde a rămas ultima dată")
    parser.add_argument("-c", "--concurrent", type=int, default=10, help="Numărul maxim de cereri concurente")
    return parser.parse_args()

async def main():
    args = parse_arguments()
    
    print_sentiment_explanation()
    
    keywords = args.keywords if args.keywords else get_keywords_from_user()
    search_mode = SearchMode.OR if args.mode == "sau" else SearchMode.AND if args.mode == "si" else get_search_mode_from_user()
    sentiment_threshold = args.threshold if args.threshold is not None else get_sentiment_threshold_from_user()
    
    resume_progress = False
    if os.path.exists('progress.pickle'):
        if args.resume is None:
            resume_progress = get_resume_choice_from_user()
        else:
            resume_progress = args.resume
    else:
        print(f"{Fore.YELLOW}Nu s-a găsit niciun progres salvat anterior. Se va începe o nouă sesiune.{Style.RESET_ALL}")
    
    crawler = WebCrawler(keywords, sentiment_threshold, search_mode, max_concurrent_requests=args.concurrent)
    await crawler.run(resume_progress)

if __name__ == "__main__":
    print(f"{Fore.CYAN}Bun venit la Web Crawler-ul cu analiză de sentiment!{Style.RESET_ALL}")
    print(f"{Fore.YELLOW}Acest script poate fi folosit pentru:{Style.RESET_ALL}")
    print("1. Căutarea de conținut web care conține anumite cuvinte cheie")
    print("2. Analiza sentimentului textelor găsite")
    print("3. Identificarea potențialului conținut negativ sau amenințător")
    print("4. Explorarea datelor din Common Crawl pentru cercetare sau analiză")
    print(f"{Fore.YELLOW}Vă rugăm să utilizați acest script în mod responsabil și în conformitate cu legile și reglementările aplicabile.{Style.RESET_ALL}\n")
    
    asyncio.run(main())
