import logging
import hashlib
import hmac
import base64
import urllib.parse
from typing import Optional, Dict, Any
from datetime import datetime
from ...shared.config.settings import settings
from ..models.payment import Payment, PaymentMethod, PaymentType, PaymentStatus
from ...bookings.models.booking import Booking, BookingStatus
from ...system.models.system_settings import SystemSettings
from sqlalchemy.orm import Session
import os

logger = logging.getLogger(__name__)

def get_borica_terminal_id(db: Session) -> Optional[str]:
    try:
        setting = db.query(SystemSettings).filter(SystemSettings.key == 'borica_terminal_id').first()
        if setting and setting.value:
            return setting.value
    except Exception:
        pass
    return settings.BORICA_TERMINAL_ID if settings.BORICA_TERMINAL_ID else None

def get_borica_merchant_id(db: Session) -> Optional[str]:
    try:
        setting = db.query(SystemSettings).filter(SystemSettings.key == 'borica_merchant_id').first()
        if setting and setting.value:
            return setting.value
    except Exception:
        pass
    return settings.BORICA_MERCHANT_ID if settings.BORICA_MERCHANT_ID else None

def get_borica_private_key_path(db: Session) -> Optional[str]:
    try:
        setting = db.query(SystemSettings).filter(SystemSettings.key == 'borica_private_key_path').first()
        if setting and setting.value:
            return setting.value
    except Exception:
        pass
    return settings.BORICA_PRIVATE_KEY_PATH if settings.BORICA_PRIVATE_KEY_PATH else None

def get_borica_certificate_path(db: Session) -> Optional[str]:
    try:
        setting = db.query(SystemSettings).filter(SystemSettings.key == 'borica_certificate_path').first()
        if setting and setting.value:
            return setting.value
    except Exception:
        pass
    return settings.BORICA_CERTIFICATE_PATH if settings.BORICA_CERTIFICATE_PATH else None

def get_borica_gateway_url(db: Session) -> str:
    try:
        setting = db.query(SystemSettings).filter(SystemSettings.key == 'borica_gateway_url').first()
        if setting and setting.value:
            return setting.value
    except Exception:
        pass
    mode = get_borica_mode(db)
    if mode == 'production':
        return settings.BORICA_GATEWAY_URL.replace('dev', 'gate') if 'dev' in settings.BORICA_GATEWAY_URL else settings.BORICA_GATEWAY_URL
    return settings.BORICA_GATEWAY_URL

def get_borica_mode(db: Session) -> str:
    try:
        setting = db.query(SystemSettings).filter(SystemSettings.key == 'borica_mode').first()
        if setting and setting.value:
            return setting.value
    except Exception:
        pass
    return settings.BORICA_MODE if settings.BORICA_MODE else 'test'

class BoricaService:
    """
    Borica payment gateway service for processing online payments.
    Borica is the Bulgarian payment gateway system.
    """
    
    @staticmethod
    def generate_transaction_id(booking_id: int) -> str:
        """Generate a unique transaction ID for Borica"""
        timestamp = datetime.utcnow().strftime('%Y%m%d%H%M%S')
        return f"BOOK{booking_id:06d}{timestamp}"
    
    @staticmethod
    def create_payment_request(
        amount: float,
        currency: str,
        order_id: str,
        description: str,
        return_url: str,
        db: Optional[Session] = None
    ) -> Dict[str, Any]:
        """
        Create a Borica payment request.
        Returns the payment form data that needs to be submitted to Borica gateway.
        """
        terminal_id = None
        merchant_id = None
        gateway_url = None
        
        if db:
            terminal_id = get_borica_terminal_id(db)
            merchant_id = get_borica_merchant_id(db)
            gateway_url = get_borica_gateway_url(db)
        
        if not terminal_id:
            terminal_id = settings.BORICA_TERMINAL_ID
        if not merchant_id:
            merchant_id = settings.BORICA_MERCHANT_ID
        if not gateway_url:
            gateway_url = get_borica_gateway_url(db) if db else settings.BORICA_GATEWAY_URL
        
        if not terminal_id or not merchant_id:
            raise ValueError('Borica Terminal ID and Merchant ID are required')
        
        # Convert amount to minor units (cents/stotinki)
        amount_minor = int(round(amount * 100))
        
        # Format amount as string with leading zeros (12 digits)
        amount_str = f"{amount_minor:012d}"
        
        # Create transaction timestamp (YYYYMMDDHHMMSS)
        transaction_timestamp = datetime.utcnow().strftime('%Y%m%d%H%M%S')
        
        # Create order description (max 125 chars)
        order_description = description[:125] if description else f"Booking Payment {order_id}"
        
        # Create signature data string
        # Format: TERMINAL=TERMINAL_ID,TRTYPE=1,ORDER=ORDER_ID,AMOUNT=AMOUNT,TIMESTAMP=TIMESTAMP
        signature_data = f"TERMINAL={terminal_id},TRTYPE=1,ORDER={order_id},AMOUNT={amount_str},TIMESTAMP={transaction_timestamp}"
        
        # For test mode, we'll use a simple HMAC signature
        # In production, you would use the private key to sign
        private_key_path = get_borica_private_key_path(db) if db else settings.BORICA_PRIVATE_KEY_PATH
        
        # Generate signature (simplified for testing - in production use proper certificate signing)
        signature = BoricaService._generate_signature(signature_data, private_key_path)
        
        return {
            'terminal_id': terminal_id,
            'merchant_id': merchant_id,
            'order_id': order_id,
            'amount': amount_str,
            'currency': currency.upper(),
            'description': order_description,
            'timestamp': transaction_timestamp,
            'signature': signature,
            'gateway_url': gateway_url,
            'return_url': return_url,
            'trtype': '1'  # Sale transaction
        }
    
    @staticmethod
    def _generate_signature(data: str, private_key_path: Optional[str] = None) -> str:
        """
        Generate signature for Borica request.
        In production, this should use the actual private key certificate.
        For testing, we'll use a simple hash.
        """
        if private_key_path and os.path.exists(private_key_path):
            try:
                # In production, you would load the private key and sign the data
                # This is a simplified version for testing
                from cryptography.hazmat.primitives import hashes, serialization
                from cryptography.hazmat.primitives.asymmetric import padding
                from cryptography.hazmat.backends import default_backend
                
                with open(private_key_path, 'rb') as key_file:
                    private_key = serialization.load_pem_private_key(
                        key_file.read(),
                        password=None,
                        backend=default_backend()
                    )
                
                # NOTE: SHA1 is required by Borica payment gateway protocol
                # This is a known security trade-off required for payment gateway compatibility
                # Monitor for Borica protocol updates that support stronger algorithms
                signature = private_key.sign(
                    data.encode('utf-8'),
                    padding.PKCS1v15(),
                    hashes.SHA1()  # nosec B303 # Required by Borica protocol - acceptable risk
                )
                return base64.b64encode(signature).decode('utf-8')
            except Exception as e:
                logger.warning(f'Failed to sign with private key, using test signature: {e}')
        
        # Fallback: simple hash for testing
        return hashlib.sha256(data.encode('utf-8')).hexdigest()[:40]
    
    @staticmethod
    def verify_response_signature(response_data: Dict[str, Any], db: Optional[Session] = None) -> bool:
        """
        Verify the signature of a Borica response.
        """
        try:
            # Extract signature from response
            signature = response_data.get('P_SIGN', '')
            if not signature:
                return False
            
            # Reconstruct signature data from response
            terminal_id = response_data.get('TERMINAL', '')
            trtype = response_data.get('TRTYPE', '')
            order_id = response_data.get('ORDER', '')
            amount = response_data.get('AMOUNT', '')
            timestamp = response_data.get('TIMESTAMP', '')
            nonce = response_data.get('NONCE', '')
            rcode = response_data.get('RC', '')
            
            # Build signature string
            signature_data = f"TERMINAL={terminal_id},TRTYPE={trtype},ORDER={order_id},AMOUNT={amount},TIMESTAMP={timestamp},NONCE={nonce},RC={rcode}"
            
            # Verify signature using certificate
            certificate_path = get_borica_certificate_path(db) if db else settings.BORICA_CERTIFICATE_PATH
            
            if certificate_path and os.path.exists(certificate_path):
                try:
                    from cryptography import x509
                    from cryptography.hazmat.backends import default_backend
                    from cryptography.hazmat.primitives import hashes
                    from cryptography.hazmat.primitives.asymmetric import padding
                    
                    with open(certificate_path, 'rb') as cert_file:
                        cert = x509.load_pem_x509_certificate(
                            cert_file.read(),
                            default_backend()
                        )
                    
                    public_key = cert.public_key()
                    signature_bytes = base64.b64decode(signature)
                    
                    # NOTE: SHA1 is required by Borica payment gateway protocol
                    # This is a known security trade-off required for payment gateway compatibility
                    public_key.verify(
                        signature_bytes,
                        signature_data.encode('utf-8'),
                        padding.PKCS1v15(),
                        hashes.SHA1()  # nosec B303 # Required by Borica protocol - acceptable risk
                    )
                    return True
                except Exception as e:
                    logger.error(f'Signature verification failed: {e}')
                    return False
            
            # For testing, accept if signature exists
            return bool(signature)
        except Exception as e:
            logger.error(f'Error verifying signature: {e}')
            return False
    
    @staticmethod
    async def confirm_payment(
        response_data: Dict[str, Any],
        db: Session,
        booking_id: Optional[int] = None
    ) -> Dict[str, Any]:
        """
        Confirm a Borica payment from the response data.
        """
        try:
            # Verify signature
            if not BoricaService.verify_response_signature(response_data, db):
                raise ValueError('Invalid payment response signature')
            
            # Extract response data
            order_id = response_data.get('ORDER', '')
            amount_str = response_data.get('AMOUNT', '')
            rcode = response_data.get('RC', '')
            rcode_msg = response_data.get('RCTEXT', '')
            
            # Convert amount from minor units
            amount = float(amount_str) / 100 if amount_str else 0.0
            
            # Get booking ID from order_id if not provided
            if not booking_id and order_id:
                # Extract booking ID from order_id (format: BOOK{booking_id}{timestamp})
                try:
                    if order_id.startswith('BOOK'):
                        booking_id = int(order_id[4:10])
                except (ValueError, IndexError):
                    pass
            
            if not booking_id:
                raise ValueError('Booking ID is required')
            
            booking = db.query(Booking).filter(Booking.id == booking_id).first()
            if not booking:
                raise ValueError('Booking not found')
            
            # Check response code (00 = success)
            if rcode != '00':
                # Payment failed
                payment = db.query(Payment).filter(
                    Payment.booking_id == booking_id,
                    Payment.transaction_id == order_id,
                    Payment.payment_method == PaymentMethod.borica
                ).first()
                
                if payment:
                    payment.payment_status = PaymentStatus.failed
                    payment.notes = f'Borica payment failed: {rcode} - {rcode_msg}'
                    db.commit()
                    db.refresh(payment)
                
                raise ValueError(f'Payment failed: {rcode} - {rcode_msg}')
            
            # Payment successful
            payment = db.query(Payment).filter(
                Payment.booking_id == booking_id,
                Payment.transaction_id == order_id,
                Payment.payment_method == PaymentMethod.borica
            ).first()
            
            if not payment:
                payment = db.query(Payment).filter(
                    Payment.booking_id == booking_id,
                    Payment.payment_method == PaymentMethod.borica,
                    Payment.payment_status == PaymentStatus.pending
                ).order_by(Payment.created_at.desc()).first()
            
            if payment:
                payment.payment_status = PaymentStatus.completed
                payment.payment_date = datetime.utcnow()
                payment.amount = amount
                payment.transaction_id = order_id
                payment.notes = f'Borica payment completed: {rcode_msg}'
            else:
                payment_type = PaymentType.full
                if booking.requires_deposit and not booking.deposit_paid:
                    payment_type = PaymentType.deposit
                
                payment = Payment(
                    booking_id=booking_id,
                    amount=amount,
                    payment_method=PaymentMethod.borica,
                    payment_type=payment_type,
                    payment_status=PaymentStatus.completed,
                    transaction_id=order_id,
                    payment_date=datetime.utcnow(),
                    notes=f'Borica payment completed: {rcode_msg}'
                )
                db.add(payment)
            
            db.commit()
            db.refresh(payment)
            
            # Update booking status if payment completed
            if payment.payment_status == PaymentStatus.completed:
                db.refresh(booking)
                total_paid = sum(
                    float(p.amount) for p in booking.payments 
                    if p.payment_status == PaymentStatus.completed
                )
                
                if payment.payment_type == PaymentType.deposit:
                    booking.deposit_paid = True
                    if booking.status in [BookingStatus.pending, BookingStatus.cancelled]:
                        booking.status = BookingStatus.confirmed
                elif payment.payment_type == PaymentType.full:
                    if total_paid >= float(booking.total_price):
                        if booking.status in [BookingStatus.pending, BookingStatus.cancelled]:
                            booking.status = BookingStatus.confirmed
                
                db.commit()
                db.refresh(booking)
            
            def get_enum_value(enum_obj):
                if enum_obj is None:
                    return None
                if isinstance(enum_obj, (PaymentMethod, PaymentType, PaymentStatus)):
                    return enum_obj.value
                return enum_obj
            
            return {
                'id': payment.id,
                'booking_id': payment.booking_id,
                'amount': float(payment.amount) if payment.amount else 0.0,
                'payment_method': get_enum_value(payment.payment_method),
                'payment_type': get_enum_value(payment.payment_type),
                'payment_status': get_enum_value(payment.payment_status),
                'transaction_id': payment.transaction_id,
                'payment_date': payment.payment_date.isoformat() if payment.payment_date else None
            }
        except ValueError as e:
            db.rollback()
            raise
        except Exception as e:
            logger.error(f'Error confirming Borica payment: {e}', exc_info=True)
            db.rollback()
            raise ValueError(f'Error confirming payment: {str(e)}')

