"""
Parallel reference processing system for RefChecker.

This module provides parallelized reference verification with ordered result output.
It maintains the same error detection quality as sequential processing while
dramatically improving performance for large bibliographies.
"""

import time
import logging
from queue import Queue
from threading import Thread, Lock
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
from typing import List, Dict, Any, Optional, Tuple, Callable
from refchecker.utils.text_utils import deduplicate_urls

logger = logging.getLogger(__name__)


@dataclass
class ReferenceWorkItem:
    """Work item for the reference verification queue."""
    index: int
    source_paper: Any
    reference: Dict[str, Any]
    timestamp: float


@dataclass
class ReferenceResult:
    """Result of a reference verification."""
    index: int
    errors: Optional[List[Dict[str, Any]]]
    url: Optional[str]
    processing_time: float
    reference: Dict[str, Any]
    verified_data: Optional[Dict[str, Any]] = None
    hallucination_assessment: Optional[Dict[str, Any]] = None
    hallucination_verdict_applied: bool = False


class ParallelReferenceProcessor:
    """
    Parallel reference verification processor with ordered output.
    
    This class manages a pool of worker threads that verify references independently
    while ensuring results are printed in the original order (1, 2, 3...).
    """
    
    def __init__(self, base_checker: Any, max_workers: int = 6, enable_progress: bool = True):
        """
        Initialize the parallel processor.
        
        Args:
            base_checker: The base reference checker instance
            max_workers: Maximum number of worker threads
            enable_progress: Whether to show progress indicators
        """
        self.base_checker = base_checker
        self.max_workers = max_workers
        self.enable_progress = enable_progress
        
        # Threading components
        self.work_queue = Queue()
        self.result_queue = Queue()
        self.result_buffer = {}  # index -> ReferenceResult
        self.buffer_lock = Lock()
        
        # State tracking
        self.next_print_index = 0
        self.total_references = 0
        self.completed_count = 0
        self.start_time = 0
        
        # Statistics
        self.processing_stats = {
            'total_processed': 0,
            'total_errors': 0,
            'avg_processing_time': 0,
            'fastest_time': float('inf'),
            'slowest_time': 0
        }
    
    def verify_references_parallel(self, source_paper: Any, bibliography: List[Dict[str, Any]], 
                                 result_callback: Optional[Callable] = None) -> Dict[str, Any]:
        """
        Verify references in parallel with ordered output.
        
        Args:
            source_paper: The source paper containing the references
            bibliography: List of references to verify
            result_callback: Optional callback for each completed result
            
        Returns:
            Dictionary with processing statistics
        """
        if not bibliography:
            logger.info("No references to verify")
            return self._get_stats()
        
        self.total_references = len(bibliography)
        self.start_time = time.time()
        self.next_print_index = 0
        self.completed_count = 0
        self.result_buffer.clear()
        
        logger.debug(f"Starting parallel verification of {self.total_references} references with {self.max_workers} workers")
        
        # Populate work queue
        for i, reference in enumerate(bibliography):
            work_item = ReferenceWorkItem(
                index=i,
                source_paper=source_paper,
                reference=reference,
                timestamp=time.time()
            )
            self.work_queue.put(work_item)
        
        # Add sentinel values to signal workers to stop (one per worker)
        for _ in range(self.max_workers):
            self.work_queue.put(None)  # None signals end of work
        
        # Start result printer thread
        printer_thread = Thread(target=self._ordered_result_printer, args=(result_callback,))
        printer_thread.daemon = True
        printer_thread.start()
        
        # Start worker threads
        with ThreadPoolExecutor(max_workers=self.max_workers, thread_name_prefix="RefWorker") as executor:
            # Submit worker tasks
            futures = []
            for worker_id in range(self.max_workers):
                future = executor.submit(self._worker_loop, worker_id)
                futures.append(future)
            
            # Wait for all workers to complete
            for future in as_completed(futures):
                try:
                    future.result()
                except Exception as e:
                    logger.error(f"Worker thread failed: {e}")
        
        # Wait for printer to finish
        printer_thread.join()
        
        # Final stats printing disabled
        # if self.enable_progress:
        #     self._print_final_stats()
        
        return self._get_stats()
    
    def _worker_loop(self, worker_id: int) -> None:
        """
        Main loop for worker threads - no timeouts, only exit when queue is empty.
        
        Args:
            worker_id: Unique identifier for this worker
        """
        processed_count = 0
        logger.debug(f"Worker {worker_id} started")
        
        while True:
            try:
                # Get work item - blocks until available
                work_item = self.work_queue.get(block=True)
                
                # Check for sentinel value (signals end of work)
                if work_item is None:
                    self.work_queue.task_done()
                    break
                
                try:
                    # Perform reference verification using base checker
                    start_time = time.time()
                    errors, url, verified_data = self.base_checker.verify_reference(
                        work_item.source_paper,
                        work_item.reference
                    )
                    processing_time = time.time() - start_time
                    
                    # Create result
                    result = ReferenceResult(
                        index=work_item.index,
                        errors=errors,
                        url=url,
                        processing_time=processing_time,
                        reference=work_item.reference,
                        verified_data=verified_data
                    )
                    
                    # Put result in queue
                    self.result_queue.put(result)
                    processed_count += 1
                    
                    logger.debug(f"Worker {worker_id} completed reference {work_item.index} in {processing_time:.2f}s")
                    
                except Exception as e:
                    # Handle verification errors gracefully
                    logger.error(f"Worker {worker_id} failed to verify reference {work_item.index}: {e}")
                    
                    error_result = ReferenceResult(
                        index=work_item.index,
                        errors=[{"error_type": "processing_failed", "error_details": f"Internal processing error: {str(e)}"}],
                        url=None,
                        processing_time=time.time() - work_item.timestamp,
                        reference=work_item.reference
                    )
                    self.result_queue.put(error_result)
                
                finally:
                    self.work_queue.task_done()
                    
            except Exception as e:
                logger.error(f"Worker {worker_id} encountered unexpected error: {e}")
                break
        
        logger.debug(f"Worker {worker_id} finished after processing {processed_count} items")
    
    def _ordered_result_printer(self, result_callback: Optional[Callable] = None) -> None:
        """
        Print results in order and handle callbacks.
        
        Args:
            result_callback: Optional callback function for each result
        """
        logger.debug("Result printer started")
        
        while self.next_print_index < self.total_references:
            try:
                # Get result - blocks until available
                result = self.result_queue.get(block=True)
                
                # Store result in buffer
                with self.buffer_lock:
                    self.result_buffer[result.index] = result
                    self._update_stats(result)
                
                # Print any consecutive results starting from next_print_index
                with self.buffer_lock:
                    while self.next_print_index in self.result_buffer:
                        current_result = self.result_buffer[self.next_print_index]
                        
                        # Run hallucination assessment BEFORE printing so we can
                        # show clean 'Verified URL' instead of 'Could not verify'
                        # Only for refs with real errors (not info/suggestion-only)
                        if current_result.errors:
                            from refchecker.core.hallucination_policy import has_real_raw_errors
                            if has_real_raw_errors(current_result.errors):
                                assessment = self.base_checker._run_and_return_hallucination_assessment(
                                    current_result.reference,
                                    current_result.errors,
                                    verified_data=current_result.verified_data,
                                    reference_url=current_result.url,
                                )
                                if assessment:
                                    current_result.hallucination_assessment = assessment
                                    # Apply verdict — re-verifies against LLM-found
                                    # metadata when available, resolving false-positive
                                    # errors from wrong-edition DB matches.
                                    from refchecker.core.hallucination_policy import apply_hallucination_verdict
                                    _has_unverified = any(
                                        e.get('error_type') == 'unverified'
                                        for e in current_result.errors
                                    )
                                    _status = 'unverified' if _has_unverified else 'error'
                                    tmp = apply_hallucination_verdict(
                                        {'status': _status, 'errors': current_result.errors},
                                        assessment,
                                        reference=current_result.reference,
                                        standard_refchecker=lambda found_ref: self.base_checker.verify_reference_standard(None, found_ref),
                                        llm_client=getattr(self.base_checker.report_builder, 'llm_verifier', None),
                                        web_searcher=getattr(self.base_checker.report_builder, 'web_searcher', None),
                                    )
                                    current_result.hallucination_assessment = tmp.get('hallucination_assessment', assessment)
                                    current_result.errors = tmp['errors']
                                    current_result.hallucination_verdict_applied = True
                        
                        # Print the result using base checker's output methods
                        # (hallucination_assessment is now available for display decisions)
                        self._print_reference_result(current_result)
                        
                        # Print hallucination flags (LLM-verified handled in _print_reference_result)
                        if current_result.hallucination_assessment:
                            verdict = current_result.hallucination_assessment.get('verdict', 'UNCERTAIN')
                            explanation = current_result.hallucination_assessment.get('explanation', '')
                            ha_link = current_result.hallucination_assessment.get('link')
                            if verdict == 'LIKELY':
                                print(f"      🚩 Likely hallucinated: {explanation}")
                            elif verdict == 'UNLIKELY' and ha_link and ha_link.startswith('http'):
                                # LLM-verified — already handled in _print_reference_result
                                pass
                            elif verdict in ('UNLIKELY', 'UNCERTAIN') and explanation:
                                has_unverified = any(
                                    e.get('error_type') == 'unverified'
                                    for e in current_result.errors
                                )
                                if has_unverified:
                                    print(f"         Not flagged: {explanation}")
                        
                        # Call callback if provided
                        if result_callback:
                            try:
                                result_callback(current_result)
                            except Exception as e:
                                logger.error(f"Result callback failed for reference {current_result.index}: {e}")
                        
                        # Clean up and advance
                        del self.result_buffer[self.next_print_index]
                        self.next_print_index += 1
                        self.completed_count += 1
                        
                        # Show progress (disabled)
                        # if self.enable_progress and self.completed_count % 10 == 0:
                        #     self._print_progress()
                
            except Exception as e:
                logger.error(f"Result printer error: {e}")
                continue
        
        logger.debug("Result printer finished")
    
    
    def _print_reference_result(self, result: ReferenceResult) -> None:
        """
        Print a single reference result using the base checker's format.
        
        Args:
            result: The reference result to print
        """
        reference = result.reference

        self.base_checker._print_reference_header(reference, result.index, self.total_references)
        llm_confirmed_without_checker_data = (
            not result.verified_data
            and result.hallucination_assessment
            and result.hallucination_assessment.get('verdict') == 'UNLIKELY'
        )
        if llm_confirmed_without_checker_data:
            print("")
        else:
            self.base_checker._print_verified_urls(reference, result.verified_data, result.url, result.errors)

        # If the ref had no DB match but the LLM confirmed it (UNLIKELY),
        # apply_hallucination_verdict already stripped the unverified error
        # and stored the link in authoritative_urls.  Show it here so the
        # user sees verification info instead of a bare reference.
        if not result.verified_data and result.hallucination_assessment:
            ha = result.hallucination_assessment
            if ha.get('verdict') == 'UNLIKELY':
                ha_link = ha.get('link')
                ha_explanation = ha.get('explanation', '')
                if ha_link and ha_link.startswith('http'):
                    print("       Matched Database: LLM search")
                    print(f"       Verified URL: {ha_link}")
                if ha_explanation:
                    print(f"       {ha_explanation}")

        # Display errors and warnings
        if result.errors:
            # Check if there's an unverified error
            has_unverified_error = any(e.get('error_type') == 'unverified' or e.get('warning_type') == 'unverified' or e.get('info_type') == 'unverified' for e in result.errors)
            
            # Check if LLM verified this reference
            llm_verified = False
            llm_verified_url = None
            assessment = result.hallucination_assessment
            if assessment and has_unverified_error:
                ha_verdict = assessment.get('verdict')
                ha_link = assessment.get('link')
                if ha_verdict == 'UNLIKELY':
                    llm_verified = True
                    if ha_link and ha_link.startswith('http'):
                        llm_verified_url = ha_link
            
            # Check if "url references paper" (webpage checker confirmed
            # the cited URL contains the paper)
            url_references_paper = any(
                'url references paper' in (e.get('error_details') or '').lower()
                for e in result.errors
            )
            
            if has_unverified_error:
                if url_references_paper and llm_verified:
                    # URL references paper AND LLM confirmed — show as verified
                    cited_url = reference.get('cited_url') or reference.get('url', '')
                    print(f"       ✅ Verified via URL: {cited_url}")
                    ha_explanation = assessment.get('explanation', '')
                    if ha_explanation:
                        print(f"         {ha_explanation}")
                elif url_references_paper and assessment and assessment.get('verdict') == 'LIKELY':
                    # URL references paper but LLM says likely hallucinated
                    self.base_checker._display_unverified_error_with_subreason(reference, result.url, result.errors, debug_mode=False, print_output=True)
                elif url_references_paper:
                    # URL references paper, no LLM or UNCERTAIN — show as verified via URL
                    cited_url = reference.get('cited_url') or reference.get('url', '')
                    print(f"       ✅ Verified via URL: {cited_url}")
                elif llm_verified and llm_verified_url:
                    # Show clean verified URL instead of 'Could not verify'
                    print("       Matched Database: LLM search")
                    print(f"       Verified URL: {llm_verified_url}")
                elif llm_verified:
                    # LLM confirmed real but no URL — keep as unverified, show explanation
                    ha_explanation = assessment.get('explanation', '')
                    self.base_checker._display_unverified_error_with_subreason(reference, result.url, result.errors, debug_mode=False, print_output=True)
                    if ha_explanation:
                        print(f"         Not flagged: {ha_explanation}")
                else:
                    # Use the centralized unverified error display function from base checker
                    self.base_checker._display_unverified_error_with_subreason(reference, result.url, result.errors, debug_mode=False, print_output=True)
            
            # Display all non-unverified errors and warnings
            self.base_checker._display_non_unverified_errors(result.errors, debug_mode=False, print_output=True)

        # Show timing info for slow references
        if result.processing_time > 5.0:
            logger.debug(f"Reference {result.index + 1} took {result.processing_time:.2f}s to verify: {reference.get('title', 'Untitled')}")
            logger.debug(f"Raw text: {reference.get('raw_text', '')}")
    
    def _update_stats(self, result: ReferenceResult) -> None:
        """Update processing statistics."""
        self.processing_stats['total_processed'] += 1
        
        if result.errors:
            self.processing_stats['total_errors'] += len(result.errors)
        
        # Update timing stats
        proc_time = result.processing_time
        self.processing_stats['fastest_time'] = min(self.processing_stats['fastest_time'], proc_time)
        self.processing_stats['slowest_time'] = max(self.processing_stats['slowest_time'], proc_time)
        
        # Update average
        total = self.processing_stats['total_processed']
        current_avg = self.processing_stats['avg_processing_time']
        self.processing_stats['avg_processing_time'] = ((current_avg * (total - 1)) + proc_time) / total
    
    def _print_progress(self) -> None:
        """Print progress information."""
        # Progress printing disabled to avoid noise
        pass
    
    def _print_final_stats(self) -> None:
        """Print final processing statistics."""
        # Final stats printing disabled to avoid noise
        pass
    
    def _get_stats(self) -> Dict[str, Any]:
        """Get processing statistics."""
        total_time = time.time() - self.start_time if self.start_time > 0 else 0
        
        return {
            'total_references': self.total_references,
            'total_time': total_time,
            'references_per_second': self.total_references / total_time if total_time > 0 else 0,
            'total_errors': self.processing_stats['total_errors'],
            'avg_processing_time': self.processing_stats['avg_processing_time'],
            'fastest_time': self.processing_stats['fastest_time'] if self.processing_stats['fastest_time'] != float('inf') else 0,
            'slowest_time': self.processing_stats['slowest_time']
        }

