"""
General Ledger service for posting transactions and managing GL operations.
"""
from sqlalchemy.orm import Session
from sqlalchemy import func, and_, or_
from typing import Optional, Dict, Any, List
from datetime import datetime, timedelta
from ..models.chart_of_accounts import ChartOfAccounts, AccountType, AccountCategory
from ..models.fiscal_period import FiscalPeriod, PeriodStatus
from ..models.journal_entry import JournalEntry, JournalLine, JournalEntryStatus
from ...shared.config.logging_config import get_logger

logger = get_logger(__name__)


class GLService:
    """Service for General Ledger operations."""
    
    # Standard account codes (can be seeded)
    ACCOUNT_CODES = {
        'cash': '1000',
        'accounts_receivable': '1100',
        'accounts_payable': '2000',
        'deferred_revenue': '2100',
        'revenue': '4000',
        'cogs': '5000',
        'operating_expenses': '6000',
        'tax_payable': '2200',
        'retained_earnings': '3000',
    }
    
    @staticmethod
    def get_or_create_account(
        db: Session,
        account_code: str,
        account_name: str,
        account_type: AccountType,
        account_category: Optional[AccountCategory] = None,
        description: Optional[str] = None
    ) -> ChartOfAccounts:
        """Get or create a chart of accounts entry."""
        account = db.query(ChartOfAccounts).filter(
            ChartOfAccounts.account_code == account_code
        ).first()
        
        if not account:
            account = ChartOfAccounts(
                account_code=account_code,
                account_name=account_name,
                account_type=account_type,
                account_category=account_category,
                description=description,
                is_active='true'
            )
            db.add(account)
            db.flush()
        
        return account
    
    @staticmethod
    def get_current_period(db: Session) -> Optional[FiscalPeriod]:
        """Get the current fiscal period."""
        return db.query(FiscalPeriod).filter(
            FiscalPeriod.is_current == True,
            FiscalPeriod.status == PeriodStatus.open
        ).first()
    
    @staticmethod
    def get_period_for_date(db: Session, date: datetime) -> Optional[FiscalPeriod]:
        """Get the fiscal period for a given date."""
        return db.query(FiscalPeriod).filter(
            FiscalPeriod.start_date <= date,
            FiscalPeriod.end_date >= date
        ).first()
    
    @staticmethod
    def create_period(
        db: Session,
        period_name: str,
        start_date: datetime,
        end_date: datetime,
        period_type: str = 'monthly'
    ) -> FiscalPeriod:
        """Create a new fiscal period."""
        # Ensure only one current period
        if period_type == 'monthly':
            db.query(FiscalPeriod).filter(FiscalPeriod.is_current == True).update({'is_current': False})
        
        period = FiscalPeriod(
            period_name=period_name,
            period_type=period_type,
            start_date=start_date,
            end_date=end_date,
            status=PeriodStatus.open,
            is_current=True
        )
        db.add(period)
        db.flush()
        return period
    
    @staticmethod
    def close_period(
        db: Session,
        period_id: int,
        closed_by: int,
        notes: Optional[str] = None
    ) -> FiscalPeriod:
        """Close a fiscal period."""
        period = db.query(FiscalPeriod).filter(FiscalPeriod.id == period_id).first()
        if not period:
            raise ValueError("Period not found")
        
        if period.status != PeriodStatus.open:
            raise ValueError(f"Period is already {period.status.value}")
        
        period.status = PeriodStatus.closed
        period.closed_by = closed_by
        period.closed_at = datetime.utcnow()
        period.notes = notes
        period.is_current = False
        
        db.flush()
        return period
    
    @staticmethod
    def generate_entry_number(db: Session, entry_date: datetime) -> str:
        """Generate a unique journal entry number."""
        date_str = entry_date.strftime('%Y%m%d')
        last_entry = db.query(JournalEntry).filter(
            JournalEntry.entry_number.like(f'JE-{date_str}-%')
        ).order_by(JournalEntry.entry_number.desc()).first()
        
        if last_entry:
            try:
                sequence = int(last_entry.entry_number.split('-')[-1])
                sequence += 1
            except (ValueError, IndexError):
                sequence = 1
        else:
            sequence = 1
        
        return f'JE-{date_str}-{sequence:04d}'
    
    @staticmethod
    def create_journal_entry(
        db: Session,
        entry_date: datetime,
        description: str,
        lines: List[Dict[str, Any]],
        reference_type: Optional[str] = None,
        reference_id: Optional[int] = None,
        created_by: int = None,
        notes: Optional[str] = None,
        auto_post: bool = False
    ) -> JournalEntry:
        """
        Create a journal entry with lines.
        Lines format: [{'account_code': '1000', 'debit': 100.0, 'credit': 0.0, 'description': '...'}, ...]
        """
        # Validate that debits equal credits
        total_debits = sum(line.get('debit', 0) or 0 for line in lines)
        total_credits = sum(line.get('credit', 0) or 0 for line in lines)
        
        if abs(total_debits - total_credits) > 0.01:
            raise ValueError(f"Journal entry is not balanced. Debits: {total_debits}, Credits: {total_credits}")
        
        # Get or create fiscal period
        period = GLService.get_period_for_date(db, entry_date)
        if not period:
            # Create a monthly period if none exists
            month_start = entry_date.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
            if month_start.month == 12:
                month_end = month_start.replace(year=month_start.year + 1, month=1) - timedelta(days=1)
            else:
                month_end = (month_start.replace(month=month_start.month + 1) - timedelta(days=1))
            month_end = month_end.replace(hour=23, minute=59, second=59)
            
            period_name = entry_date.strftime('%Y-%m')
            period = GLService.create_period(db, period_name, month_start, month_end, 'monthly')
        
        # Check if period is closed
        if period.status == PeriodStatus.closed:
            raise ValueError(f"Cannot post to closed period: {period.period_name}")
        
        # Generate entry number
        entry_number = GLService.generate_entry_number(db, entry_date)
        
        # Create journal entry
        entry = JournalEntry(
            entry_number=entry_number,
            entry_date=entry_date,
            description=description,
            fiscal_period_id=period.id,
            reference_type=reference_type,
            reference_id=reference_id,
            created_by=created_by,
            notes=notes,
            status=JournalEntryStatus.draft
        )
        db.add(entry)
        db.flush()
        
        # Create journal lines
        for idx, line_data in enumerate(lines, start=1):
            account_code = line_data.get('account_code')
            if not account_code:
                raise ValueError(f"Line {idx} missing account_code")
            
            # Get or create account
            account = db.query(ChartOfAccounts).filter(
                ChartOfAccounts.account_code == account_code
            ).first()
            
            if not account:
                raise ValueError(f"Account {account_code} not found in chart of accounts")
            
            journal_line = JournalLine(
                journal_entry_id=entry.id,
                line_number=idx,
                account_id=account.id,
                debit_amount=line_data.get('debit', 0) or 0,
                credit_amount=line_data.get('credit', 0) or 0,
                description=line_data.get('description')
            )
            db.add(journal_line)
        
        # Auto-post if requested
        if auto_post:
            GLService.post_entry(db, entry.id, created_by)
        
        db.flush()
        return entry
    
    @staticmethod
    def post_entry(db: Session, entry_id: int, posted_by: int) -> JournalEntry:
        """Post a journal entry (change status from draft to posted)."""
        entry = db.query(JournalEntry).filter(JournalEntry.id == entry_id).first()
        if not entry:
            raise ValueError("Journal entry not found")
        
        if entry.status != JournalEntryStatus.draft:
            raise ValueError(f"Entry is already {entry.status.value}")
        
        # Check period status
        period = db.query(FiscalPeriod).filter(FiscalPeriod.id == entry.fiscal_period_id).first()
        if period and period.status == PeriodStatus.closed:
            raise ValueError(f"Cannot post to closed period: {period.period_name}")
        
        entry.status = JournalEntryStatus.posted
        entry.posted_by = posted_by
        entry.posted_at = datetime.utcnow()
        
        db.flush()
        return entry
    
    @staticmethod
    def get_trial_balance(
        db: Session,
        period_id: Optional[int] = None,
        as_of_date: Optional[datetime] = None
    ) -> Dict[str, Any]:
        """Generate trial balance from journal entries."""
        query = db.query(
            ChartOfAccounts.id,
            ChartOfAccounts.account_code,
            ChartOfAccounts.account_name,
            ChartOfAccounts.account_type,
            func.sum(JournalLine.debit_amount).label('total_debits'),
            func.sum(JournalLine.credit_amount).label('total_credits')
        ).join(
            JournalLine, ChartOfAccounts.id == JournalLine.account_id
        ).join(
            JournalEntry, JournalLine.journal_entry_id == JournalEntry.id
        ).filter(
            JournalEntry.status == JournalEntryStatus.posted
        )
        
        if period_id:
            query = query.filter(JournalEntry.fiscal_period_id == period_id)
        elif as_of_date:
            query = query.filter(JournalEntry.entry_date <= as_of_date)
        
        results = query.group_by(
            ChartOfAccounts.id,
            ChartOfAccounts.account_code,
            ChartOfAccounts.account_name,
            ChartOfAccounts.account_type
        ).all()
        
        trial_balance = []
        total_debits = 0.0
        total_credits = 0.0
        
        for result in results:
            debits = float(result.total_debits or 0)
            credits = float(result.total_credits or 0)
            balance = debits - credits
            
            trial_balance.append({
                'account_code': result.account_code,
                'account_name': result.account_name,
                'account_type': result.account_type.value,
                'debits': debits,
                'credits': credits,
                'balance': balance
            })
            
            total_debits += debits
            total_credits += credits
        
        return {
            'trial_balance': trial_balance,
            'total_debits': total_debits,
            'total_credits': total_credits,
            'is_balanced': abs(total_debits - total_credits) < 0.01
        }


# Singleton instance
gl_service = GLService()

