"""
Barcode generation utilities for betting system
Supports major barcode standards used in retail and point-of-sale systems
"""

import logging
import base64
from typing import Optional, Tuple, Dict, Any
from io import BytesIO

logger = logging.getLogger(__name__)

# Supported barcode standards for retail/POS systems
BARCODE_STANDARDS = {
    'none': 'No Barcode',
    'code128': 'Code 128 (Alphanumeric)',
    'code39': 'Code 39 (Alphanumeric)',
    'ean13': 'EAN-13 (13 digits)',
    'ean8': 'EAN-8 (8 digits)',
    'upca': 'UPC-A (12 digits)',
    'upce': 'UPC-E (8 digits)',
    'codabar': 'Codabar (Numeric)',
    'itf': 'ITF (Interleaved 2 of 5)'
}

def get_supported_barcode_standards() -> Dict[str, str]:
    """Get dictionary of supported barcode standards"""
    return BARCODE_STANDARDS.copy()

def validate_barcode_data(data: str, standard: str) -> bool:
    """
    Validate data for specific barcode standard
    
    Args:
        data: Data to encode in barcode
        standard: Barcode standard to validate against
        
    Returns:
        True if data is valid for the standard
    """
    if standard == 'none':
        return True
        
    if not data or not standard:
        return False
        
    # Remove any whitespace
    data = data.strip()
    
    try:
        if standard == 'code128':
            # Code 128 accepts alphanumeric characters
            return len(data) <= 48 and all(ord(c) >= 32 and ord(c) <= 126 for c in data)
            
        elif standard == 'code39':
            # Code 39 accepts uppercase letters, digits, and some symbols
            valid_chars = set('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%')
            return len(data) <= 43 and all(c in valid_chars for c in data.upper())
            
        elif standard == 'ean13':
            # EAN-13 requires exactly 13 digits (12 data + 1 check digit)
            return data.isdigit() and len(data) == 13
            
        elif standard == 'ean8':
            # EAN-8 requires exactly 8 digits (7 data + 1 check digit)
            return data.isdigit() and len(data) == 8

        elif standard == 'upca':
            # UPC-A requires exactly 12 digits (11 data + 1 check digit)
            return data.isdigit() and len(data) == 12

        elif standard == 'upce':
            # UPC-E requires exactly 8 digits (7 data + 1 check digit)
            return data.isdigit() and len(data) == 8
            
        elif standard == 'codabar':
            # Codabar accepts digits and some special characters
            valid_chars = set('0123456789-$:/.+ABCD')
            return len(data) <= 16 and all(c in valid_chars for c in data.upper())
            
        elif standard == 'itf':
            # ITF requires even number of digits
            return data.isdigit() and len(data) % 2 == 0 and len(data) <= 30
            
        else:
            return False
            
    except Exception as e:
        logger.error(f"Barcode validation error for {standard}: {e}")
        return False

def generate_barcode_image(data: str, standard: str, width: int = 300, height: int = 100) -> Optional[bytes]:
    """
    Generate barcode image as PNG bytes

    Args:
        data: Data to encode in barcode
        standard: Barcode standard (code128, code39, ean13, etc.)
        width: Image width in pixels
        height: Image height in pixels

    Returns:
        PNG image bytes or None if generation failed
    """
    if standard == 'none':
        return None

    try:
        # Special case for UPC-E which is not supported by python-barcode library
        if standard == 'upce':
            logger.warning(f"UPC-E barcode generation not supported by python-barcode library. Data: {data}")
            return None

        # Validate data first
        if not validate_barcode_data(data, standard):
            logger.error(f"Invalid data '{data}' for barcode standard '{standard}'")
            return None

        # Try to import barcode library
        try:
            from barcode import get_barcode_class
            from barcode.writer import ImageWriter
        except ImportError:
            logger.error("Python barcode library not available. Install with: pip install python-barcode[images]")
            return None

        # Map our standard names to barcode library names
        standard_mapping = {
            'code128': 'code128',
            'code39': 'code39',
            'ean13': 'ean13',
            'ean8': 'ean8',
            'upca': 'upc',
            'codabar': 'codabar',
            'itf': 'itf'
        }

        barcode_type = standard_mapping.get(standard)
        if not barcode_type:
            logger.error(f"Unsupported barcode standard: {standard}")
            return None

        # Get barcode class
        try:
            barcode_class = get_barcode_class(barcode_type)
        except Exception as e:
            logger.error(f"Failed to get barcode class for {barcode_type}: {e}")
            return None

        # Create image writer with custom options
        writer = ImageWriter()
        writer.set_options({
            'module_width': 0.2,
            'module_height': height / 100.0,
            'font_size': 10,
            'text_distance': 5.0,
            'background': 'white',
            'foreground': 'black',
            'write_text': True,
            'text': data
        })

        # Create barcode
        barcode_instance = barcode_class(data, writer=writer)

        # Get PIL Image using render() method
        pil_image = barcode_instance.render()

        # Save to bytes
        buffer = BytesIO()
        pil_image.save(buffer, format='PNG')
        buffer.seek(0)

        image_bytes = buffer.getvalue()
        buffer.close()

        logger.debug(f"Generated {standard} barcode for data: {data}")
        return image_bytes

    except Exception as e:
        logger.error(f"Failed to generate barcode: {e}")
        return None

def generate_barcode_base64(data: str, standard: str, width: int = 300, height: int = 100) -> Optional[str]:
    """
    Generate barcode as base64 encoded string for embedding in HTML
    
    Args:
        data: Data to encode in barcode
        standard: Barcode standard
        width: Image width in pixels
        height: Image height in pixels
        
    Returns:
        Base64 encoded PNG string or None if generation failed
    """
    try:
        image_bytes = generate_barcode_image(data, standard, width, height)
        if image_bytes:
            encoded = base64.b64encode(image_bytes).decode('utf-8')
            return f"data:image/png;base64,{encoded}"
        return None
        
    except Exception as e:
        logger.error(f"Failed to generate base64 barcode: {e}")
        return None

def calculate_ean13_check_digit(data: str) -> str:
    """
    Calculate EAN-13 check digit for the given 12-digit data

    Args:
        data: 12-digit string

    Returns:
        Complete 13-digit EAN-13 code including check digit
    """
    if not (data.isdigit() and len(data) == 12):
        raise ValueError("EAN-13 check digit calculation requires exactly 12 digits")

    # EAN-13 check digit calculation
    # Weights alternate starting with 1 for leftmost digit, 3 for next, etc.
    # This ensures the rightmost data digit gets weight 3
    weights = [1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3]
    total = sum(int(data[i]) * weights[i] for i in range(12))

    # Find the smallest number that makes total divisible by 10
    check_digit = (10 - (total % 10)) % 10

    return data + str(check_digit)


def calculate_ean8_check_digit(data: str) -> str:
    """
    Calculate EAN-8 check digit for the given 7-digit data

    Args:
        data: 7-digit string

    Returns:
        Complete 8-digit EAN-8 code including check digit
    """
    if not (data.isdigit() and len(data) == 7):
        raise ValueError("EAN-8 check digit calculation requires exactly 7 digits")

    # EAN-8 uses the same algorithm as EAN-13
    # Step 1: Sum digits in odd positions (1st, 3rd, 5th, 7th) multiplied by 3
    odd_sum = sum(int(data[i]) for i in range(0, 7, 2)) * 3

    # Step 2: Sum digits in even positions (2nd, 4th, 6th)
    even_sum = sum(int(data[i]) for i in range(1, 7, 2))

    # Step 3: Total sum
    total = odd_sum + even_sum

    # Step 4: Find the smallest number that makes total divisible by 10
    check_digit = (10 - (total % 10)) % 10

    return data + str(check_digit)


def calculate_upca_check_digit(data: str) -> str:
    """
    Calculate UPC-A check digit for the given 11-digit data

    Args:
        data: 11-digit string

    Returns:
        Complete 12-digit UPC-A code including check digit
    """
    if not (data.isdigit() and len(data) == 11):
        raise ValueError("UPC-A check digit calculation requires exactly 11 digits")

    # UPC-A check digit calculation (different from EAN-13)
    # Step 1: Sum digits in odd positions (1st, 3rd, 5th, 7th, 9th, 11th) multiplied by 3
    odd_sum = sum(int(data[i]) for i in range(0, 11, 2)) * 3

    # Step 2: Sum digits in even positions (2nd, 4th, 6th, 8th, 10th)
    even_sum = sum(int(data[i]) for i in range(1, 11, 2))

    # Step 3: Total sum
    total = odd_sum + even_sum

    # Step 4: Find the smallest number that makes total divisible by 10
    check_digit = (10 - (total % 10)) % 10

    return data + str(check_digit)


def calculate_upce_check_digit(data: str) -> str:
    """
    Calculate UPC-E check digit for the given 7-digit data

    Args:
        data: 7-digit string

    Returns:
        Complete 8-digit UPC-E code including check digit
    """
    if not (data.isdigit() and len(data) == 7):
        raise ValueError("UPC-E check digit calculation requires exactly 7 digits")

    # UPC-E check digit calculation
    # First expand to UPC-A format, then calculate UPC-A check digit
    # This is a simplified approach - UPC-E has complex expansion rules
    # For our purposes, we'll use the same algorithm as UPC-A
    odd_sum = sum(int(data[i]) for i in range(0, 7, 2)) * 3
    even_sum = sum(int(data[i]) for i in range(1, 7, 2))
    total = odd_sum + even_sum
    check_digit = (10 - (total % 10)) % 10

    return data + str(check_digit)


def format_bet_id_for_barcode(bet_uuid: str, standard: str, counter: int = 0) -> str:
    """
    Format bet UUID for specific barcode standard

    Args:
        bet_uuid: Original bet UUID
        standard: Target barcode standard
        counter: Counter to add uniqueness when duplicates occur

    Returns:
        Formatted data suitable for the barcode standard
    """
    try:
        if standard == 'none':
            return bet_uuid

        # Remove hyphens and convert to uppercase
        clean_uuid = bet_uuid.replace('-', '').upper()

        if standard in ['code128', 'code39']:
            # These support alphanumeric, use full UUID for maximum uniqueness
            # Add counter to ensure uniqueness if needed
            if counter > 0:
                clean_uuid = f"{clean_uuid}_{counter}"
            return clean_uuid

        elif standard in ['ean13', 'ean8', 'upca', 'upce', 'itf', 'codabar']:
            # These require numeric data
            # Convert hex UUID to numeric by taking hash
            # Include counter in hash to ensure uniqueness
            import hashlib
            hash_input = f"{bet_uuid}_{counter}".encode()
            hash_obj = hashlib.md5(hash_input)
            numeric_hash = str(int(hash_obj.hexdigest()[:12], 16))

            if standard == 'ean13':
                # EAN-13 needs exactly 12 digits, then we add check digit to make 13
                data_12 = numeric_hash[:12].zfill(12)
                return calculate_ean13_check_digit(data_12)
            elif standard == 'ean8':
                # EAN-8 needs exactly 7 digits, then we add check digit to make 8
                data_7 = numeric_hash[:7].zfill(7)
                return calculate_ean8_check_digit(data_7)
            elif standard == 'upca':
                # UPC-A needs exactly 11 digits, then we add check digit to make 12
                data_11 = numeric_hash[:11].zfill(11)
                return calculate_upca_check_digit(data_11)
            elif standard == 'upce':
                # UPC-E needs exactly 7 digits, then we add check digit to make 8
                data_7 = numeric_hash[:7].zfill(7)
                return calculate_upce_check_digit(data_7)
            elif standard == 'codabar':
                # Codabar can use digits with start/stop characters
                return f"A{numeric_hash[:14]}A"
            elif standard == 'itf':
                # ITF needs even number of digits
                numeric_data = numeric_hash[:16].zfill(16)
                return numeric_data[:16] if len(numeric_data) % 2 == 0 else numeric_data[:15] + '0'

        return clean_uuid[:16]  # Fallback

    except Exception as e:
        logger.error(f"Failed to format bet ID for barcode: {e}")
        return bet_uuid[:16]  # Safe fallback

def create_bet_barcode_data(bet_uuid: str, barcode_config: Dict[str, Any]) -> Optional[Dict[str, Any]]:
    """
    Create complete barcode data for a bet including image and metadata
    
    Args:
        bet_uuid: Bet UUID
        barcode_config: Barcode configuration from database
        
    Returns:
        Dictionary with barcode data or None if disabled
    """
    try:
        standard = barcode_config.get('standard', 'none')
        
        if standard == 'none':
            return None
            
        # Format bet ID for the barcode standard
        barcode_data = format_bet_id_for_barcode(bet_uuid, standard)
        
        # Generate barcode image
        barcode_base64 = generate_barcode_base64(barcode_data, standard)
        
        if not barcode_base64:
            logger.error(f"Failed to generate barcode image for bet {bet_uuid}")
            return None
            
        return {
            'standard': standard,
            'standard_name': BARCODE_STANDARDS.get(standard, standard),
            'data': barcode_data,
            'original_uuid': bet_uuid,
            'image_base64': barcode_base64,
            'width': barcode_config.get('width', 300),
            'height': barcode_config.get('height', 100)
        }
        
    except Exception as e:
        logger.error(f"Failed to create bet barcode data: {e}")
        return None

def reverse_lookup_bet_uuid(barcode_data: str, standard: str) -> Optional[str]:
    """
    Attempt to reverse lookup the original bet UUID from barcode data
    This is limited due to hash-based conversion for numeric standards
    
    Args:
        barcode_data: Scanned barcode data
        standard: Barcode standard used
        
    Returns:
        Possible bet UUID or None if cannot determine
    """
    try:
        if standard == 'none':
            return barcode_data
            
        if standard in ['code128', 'code39']:
            # For alphanumeric standards, we stored first 16 chars of clean UUID
            # Try to find matching bet in database by searching for UUIDs that match
            return None  # Requires database query
            
        else:
            # For numeric standards, we used MD5 hash conversion
            # Cannot reliably reverse this - need database lookup
            return None
            
    except Exception as e:
        logger.error(f"Failed to reverse lookup bet UUID: {e}")
        return None

def test_barcode_generation():
    """Test barcode generation for all supported standards"""
    test_results = {}
    test_uuid = "12345678-1234-5678-9abc-123456789def"
    
    for standard, name in BARCODE_STANDARDS.items():
        if standard == 'none':
            continue
            
        try:
            formatted_data = format_bet_id_for_barcode(test_uuid, standard)
            is_valid = validate_barcode_data(formatted_data, standard)
            
            if is_valid:
                image_bytes = generate_barcode_image(formatted_data, standard)
                success = image_bytes is not None
            else:
                success = False
                
            test_results[standard] = {
                'name': name,
                'test_data': formatted_data,
                'valid_data': is_valid,
                'generation_success': success,
                'error': None if success else 'Validation or generation failed'
            }
            
        except Exception as e:
            test_results[standard] = {
                'name': name,
                'test_data': None,
                'valid_data': False,
                'generation_success': False,
                'error': str(e)
            }
            
    return test_results