"""
Cost Allocation Engine for MarkiTect Issue Cost Distribution.

This module implements the core allocation engine that distributes operational
costs across active issues using the defined algorithm from Issue #88.

The engine handles:
- Equal distribution of costs across active issues in a period
- Loss carried forward when no active issues exist
- Transaction audit trail creation
- Edge case handling and validation
- Integration with period management and activity tracking
"""

import sqlite3
from datetime import date, datetime
from decimal import Decimal
from typing import List, Dict, Any, Optional, Tuple
from dataclasses import dataclass
from enum import Enum

from .models import FinanceModels
from .cost_manager import CostItemManager
from .period_manager import PeriodManager, Period, PeriodStatus
from ..issues.activity_tracker import IssueActivityTracker, ActivityType


class AllocationStatus(Enum):
    """Status enumeration for allocation operations."""
    SUCCESS = "success"
    NO_ACTIVE_ISSUES = "no_active_issues"
    NO_COSTS_TO_ALLOCATE = "no_costs_to_allocate"
    PERIOD_CLOSED = "period_closed"
    ERROR = "error"


@dataclass
class AllocationResult:
    """Result of a cost allocation operation."""
    status: AllocationStatus
    period_id: int
    total_costs: Decimal = Decimal('0.00')
    active_issues: List[int] = None
    cost_per_issue: Decimal = Decimal('0.00')
    allocations_created: int = 0
    transactions_created: int = 0
    loss_carried_forward: Decimal = Decimal('0.00')
    message: str = ""

    def __post_init__(self):
        if self.active_issues is None:
            self.active_issues = []


@dataclass
class IssueAllocation:
    """Represents a cost allocation to a specific issue."""
    issue_id: int
    allocated_amount: Decimal
    allocation_date: date
    period_id: int
    transaction_id: Optional[int] = None


class TransactionManager:
    """Manages cost transaction audit trails for allocations."""

    def __init__(self, db_path: str):
        """
        Initialize transaction manager.

        Args:
            db_path: Path to the SQLite database
        """
        self.db_path = db_path
        self.finance_models = FinanceModels(db_path)

    def create_allocation_transaction(
        self,
        period_id: int,
        amount: Decimal,
        issue_id: int,
        transaction_date: date,
        description: str
    ) -> int:
        """
        Create a cost allocation transaction record.

        Args:
            period_id: ID of the cost period
            amount: Amount allocated to the issue
            issue_id: ID of the issue receiving allocation
            transaction_date: Date of the transaction
            description: Description of the allocation

        Returns:
            ID of the created transaction
        """
        with self.finance_models.get_connection() as conn:
            cursor = conn.cursor()

            cursor.execute('''
                INSERT INTO cost_transactions
                (period_id, transaction_type, amount_eur, issue_id,
                 transaction_date, description)
                VALUES (?, 'cost_allocated', ?, ?, ?, ?)
            ''', (period_id, float(amount), issue_id, transaction_date.isoformat() if hasattr(transaction_date, 'isoformat') else transaction_date, description))

            return cursor.lastrowid

    def create_loss_forward_transaction(
        self,
        from_period_id: int,
        to_period_id: int,
        amount: Decimal,
        transaction_date: date,
        description: str
    ) -> int:
        """
        Create a loss carried forward transaction.

        Args:
            from_period_id: Source period ID
            to_period_id: Destination period ID
            amount: Amount being carried forward
            transaction_date: Date of the transaction
            description: Description of the carry forward

        Returns:
            ID of the created transaction
        """
        with self.finance_models.get_connection() as conn:
            cursor = conn.cursor()

            cursor.execute('''
                INSERT INTO cost_transactions
                (period_id, transaction_type, amount_eur, transaction_date, description)
                VALUES (?, 'loss_forward', ?, ?, ?)
            ''', (to_period_id, float(amount), transaction_date.isoformat() if hasattr(transaction_date, 'isoformat') else transaction_date, description))

            return cursor.lastrowid


class AllocationEngine:
    """
    Core cost allocation engine for distributing operational costs to active issues.

    Implements the algorithm defined in Issue #88:
    1. Calculate total costs for the period (monthly + one-time + carried forward)
    2. Identify active issues (created/modified during period)
    3. Distribute costs equally among active issues
    4. Handle edge cases (no active issues -> carry forward loss)
    5. Create audit trail transactions
    6. Update period statistics
    """

    def __init__(self, db_path: str = "markitect.db"):
        """
        Initialize the allocation engine.

        Args:
            db_path: Path to the SQLite database
        """
        self.db_path = db_path
        self.finance_models = FinanceModels(db_path)
        self.cost_manager = CostItemManager(db_path)
        self.period_manager = PeriodManager(db_path)
        self.activity_tracker = IssueActivityTracker(db_path)
        self.transaction_manager = TransactionManager(db_path)

        # Ensure database schema is initialized
        self.finance_models.initialize_finance_schema()

    def allocate_period_costs(self, period_id: int) -> AllocationResult:
        """
        Allocate costs for a specific period to active issues.

        Args:
            period_id: ID of the period to process

        Returns:
            AllocationResult with operation details and status
        """
        try:
            # Get period details
            period = self._get_period(period_id)
            if not period:
                return AllocationResult(
                    status=AllocationStatus.ERROR,
                    period_id=period_id,
                    message=f"Period {period_id} not found"
                )

            # Check if period is already closed
            if period.status == PeriodStatus.CLOSED.value:
                return AllocationResult(
                    status=AllocationStatus.PERIOD_CLOSED,
                    period_id=period_id,
                    message=f"Period {period_id} is already closed"
                )

            # Set period status to calculating
            self._update_period_status(period_id, PeriodStatus.CALCULATING)

            # Step 1: Calculate total costs for period
            total_costs = self._calculate_period_total_costs(period)

            if total_costs == Decimal('0.00'):
                self._update_period_status(period_id, PeriodStatus.CLOSED)
                return AllocationResult(
                    status=AllocationStatus.NO_COSTS_TO_ALLOCATE,
                    period_id=period_id,
                    total_costs=total_costs,
                    message="No costs to allocate for this period"
                )

            # Step 2: Identify active issues for the period
            active_issues = self._get_active_issues_for_period(period)

            if not active_issues:
                # No active issues - carry forward loss to next period
                next_period_id = self._get_or_create_next_period(period)
                if next_period_id:
                    self._carry_forward_loss(period_id, next_period_id, total_costs)

                # Update period and close
                self._update_period_totals(period_id, total_costs, 0, Decimal('0.00'), total_costs)
                self._update_period_status(period_id, PeriodStatus.CLOSED)

                return AllocationResult(
                    status=AllocationStatus.NO_ACTIVE_ISSUES,
                    period_id=period_id,
                    total_costs=total_costs,
                    active_issues=[],
                    loss_carried_forward=total_costs,
                    message=f"No active issues found. Carried forward €{total_costs:.2f} to next period"
                )

            # Step 3: Calculate cost per issue (equal distribution)
            cost_per_issue = total_costs / len(active_issues)

            # Step 4: Create allocations and transactions
            allocations_created = 0
            transactions_created = 0
            allocation_date = date.today()

            for issue_id in active_issues:
                # Create allocation record
                allocation_id = self._create_issue_allocation(
                    issue_id, period_id, cost_per_issue, allocation_date
                )

                if allocation_id:
                    allocations_created += 1

                    # Create audit transaction
                    transaction_id = self.transaction_manager.create_allocation_transaction(
                        period_id=period_id,
                        amount=cost_per_issue,
                        issue_id=issue_id,
                        transaction_date=allocation_date,
                        description=f"Cost allocation for period {period.period_start} to {period.period_end}"
                    )

                    if transaction_id:
                        transactions_created += 1
                        # Link transaction to allocation
                        self._update_allocation_transaction_id(allocation_id, transaction_id)

            # Step 5: Update period totals
            self._update_period_totals(
                period_id, total_costs, len(active_issues), cost_per_issue, Decimal('0.00')
            )

            # Step 6: Close the period
            self._update_period_status(period_id, PeriodStatus.CLOSED)

            return AllocationResult(
                status=AllocationStatus.SUCCESS,
                period_id=period_id,
                total_costs=total_costs,
                active_issues=active_issues,
                cost_per_issue=cost_per_issue,
                allocations_created=allocations_created,
                transactions_created=transactions_created,
                message=f"Successfully allocated €{total_costs:.2f} across {len(active_issues)} issues"
            )

        except Exception as e:
            # Reset period status on error
            self._update_period_status(period_id, PeriodStatus.OPEN)
            return AllocationResult(
                status=AllocationStatus.ERROR,
                period_id=period_id,
                message=f"Allocation failed: {str(e)}"
            )

    def get_issue_allocations(self, issue_id: int) -> List[Dict[str, Any]]:
        """
        Get all cost allocations for a specific issue.

        Args:
            issue_id: ID of the issue

        Returns:
            List of allocation records
        """
        with self.finance_models.get_connection() as conn:
            cursor = conn.cursor()

            cursor.execute('''
                SELECT
                    ica.id,
                    ica.issue_id,
                    ica.period_id,
                    ica.allocated_amount,
                    ica.allocation_date,
                    ica.transaction_id,
                    cp.period_start,
                    cp.period_end,
                    cp.period_type
                FROM issue_cost_allocations ica
                JOIN cost_periods cp ON ica.period_id = cp.id
                WHERE ica.issue_id = ?
                ORDER BY ica.allocation_date DESC
            ''', (issue_id,))

            rows = cursor.fetchall()
            allocations = []

            for row in rows:
                allocation = {
                    'id': row[0],
                    'issue_id': row[1],
                    'period_id': row[2],
                    'allocated_amount': float(row[3]),
                    'allocation_date': row[4],
                    'transaction_id': row[5],
                    'period_start': row[6],
                    'period_end': row[7],
                    'period_type': row[8]
                }
                allocations.append(allocation)

            return allocations

    def get_period_allocations(self, period_id: int) -> List[Dict[str, Any]]:
        """
        Get all allocations for a specific period.

        Args:
            period_id: ID of the period

        Returns:
            List of allocation records
        """
        with self.finance_models.get_connection() as conn:
            cursor = conn.cursor()

            cursor.execute('''
                SELECT
                    ica.id,
                    ica.issue_id,
                    ica.allocated_amount,
                    ica.allocation_date,
                    ica.transaction_id
                FROM issue_cost_allocations ica
                WHERE ica.period_id = ?
                ORDER BY ica.issue_id
            ''', (period_id,))

            rows = cursor.fetchall()
            allocations = []

            for row in rows:
                allocation = {
                    'id': row[0],
                    'issue_id': row[1],
                    'allocated_amount': float(row[2]),
                    'allocation_date': row[3],
                    'transaction_id': row[4]
                }
                allocations.append(allocation)

            return allocations

    def reverse_allocation(self, allocation_id: int) -> bool:
        """
        Reverse a cost allocation (for corrections).

        Args:
            allocation_id: ID of the allocation to reverse

        Returns:
            True if successful, False otherwise
        """
        try:
            with self.finance_models.get_connection() as conn:
                cursor = conn.cursor()

                # Get allocation details
                cursor.execute('''
                    SELECT issue_id, period_id, allocated_amount, transaction_id
                    FROM issue_cost_allocations
                    WHERE id = ?
                ''', (allocation_id,))

                result = cursor.fetchone()
                if not result:
                    return False

                issue_id, period_id, amount, transaction_id = result

                # Create reversal transaction using adjustment type (allows negative amounts)
                with self.finance_models.get_connection() as conn2:
                    cursor2 = conn2.cursor()
                    cursor2.execute('''
                        INSERT INTO cost_transactions
                        (period_id, transaction_type, amount_eur, issue_id,
                         transaction_date, description)
                        VALUES (?, 'adjustment', ?, ?, ?, ?)
                    ''', (period_id, float(-amount), issue_id, date.today().isoformat(), f"Reversal of allocation #{allocation_id}"))

                    reversal_transaction_id = cursor2.lastrowid

                # Only delete if reversal transaction was created successfully
                if reversal_transaction_id:
                    cursor.execute('DELETE FROM issue_cost_allocations WHERE id = ?', (allocation_id,))
                    return cursor.rowcount > 0
                else:
                    return False

        except Exception as e:
            # Log the exception for debugging in tests
            print(f"Reversal failed with exception: {e}")
            return False

    def _get_period(self, period_id: int) -> Optional[Period]:
        """Get period details by ID."""
        period_data = self.period_manager.get_period_by_id(period_id)
        if not period_data:
            return None

        # Convert dict to Period object
        return Period(
            id=period_data['id'],
            period_start=datetime.strptime(period_data['period_start'], '%Y-%m-%d').date() if period_data['period_start'] else None,
            period_end=datetime.strptime(period_data['period_end'], '%Y-%m-%d').date() if period_data['period_end'] else None,
            period_type=period_data['period_type'],
            status=period_data['status'],
            total_costs=Decimal(str(period_data['total_costs'])),
            active_issues_count=period_data['active_issues_count'],
            cost_per_issue=Decimal(str(period_data['cost_per_issue'])),
            loss_carried_forward=Decimal(str(period_data['loss_carried_forward'] or 0))
        )

    def _update_period_status(self, period_id: int, status: PeriodStatus):
        """Update period status."""
        with self.finance_models.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute(
                'UPDATE cost_periods SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
                (status.value, period_id)
            )

    def _calculate_period_total_costs(self, period: Period) -> Decimal:
        """Calculate total costs for a period including carried forward amounts."""
        calculations = self.cost_manager.calculate_period_costs(
            period.period_start, period.period_end
        )

        period_costs = calculations['total_period']
        carried_forward = period.loss_carried_forward or Decimal('0.00')

        return Decimal(str(period_costs)) + carried_forward

    def _get_active_issues_for_period(self, period: Period) -> List[int]:
        """Get list of active issue IDs for a period."""
        activities = self.activity_tracker.get_activities_by_period(
            period.id,
            activity_types=[
                ActivityType.CREATED,
                ActivityType.MODIFIED,
                ActivityType.COMMENTED,
                ActivityType.STATUS_CHANGED
            ]
        )

        # Get unique issue IDs
        active_issues = list(set(activity.issue_id for activity in activities))
        return active_issues

    def _get_or_create_next_period(self, current_period: Period) -> Optional[int]:
        """Get or create the next period for loss carry forward."""
        # For now, return None - next period creation will be handled separately
        # This is a placeholder for future automatic period creation
        return None

    def _carry_forward_loss(self, from_period_id: int, to_period_id: int, amount: Decimal):
        """Carry forward loss to next period."""
        # Update the destination period's carried forward amount
        with self.finance_models.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute('''
                UPDATE cost_periods
                SET loss_carried_forward = loss_carried_forward + ?
                WHERE id = ?
            ''', (float(amount), to_period_id))

            # Create audit transaction
            self.transaction_manager.create_loss_forward_transaction(
                from_period_id=from_period_id,
                to_period_id=to_period_id,
                amount=amount,
                transaction_date=date.today(),
                description=f"Loss carried forward from period {from_period_id}"
            )

    def _create_issue_allocation(
        self, issue_id: int, period_id: int, amount: Decimal, allocation_date: date
    ) -> Optional[int]:
        """Create an issue cost allocation record."""
        try:
            with self.finance_models.get_connection() as conn:
                cursor = conn.cursor()
                cursor.execute('''
                    INSERT INTO issue_cost_allocations
                    (issue_id, period_id, allocated_amount, allocation_date)
                    VALUES (?, ?, ?, ?)
                ''', (issue_id, period_id, float(amount), allocation_date.isoformat() if hasattr(allocation_date, 'isoformat') else allocation_date))

                return cursor.lastrowid
        except sqlite3.IntegrityError:
            # Allocation already exists for this issue/period
            return None

    def _update_allocation_transaction_id(self, allocation_id: int, transaction_id: int):
        """Link allocation to its audit transaction."""
        with self.finance_models.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute('''
                UPDATE issue_cost_allocations
                SET transaction_id = ?
                WHERE id = ?
            ''', (transaction_id, allocation_id))

    def _update_period_totals(
        self,
        period_id: int,
        total_costs: Decimal,
        active_issues_count: int,
        cost_per_issue: Decimal,
        loss_carried_forward: Decimal
    ):
        """Update period summary statistics."""
        with self.finance_models.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute('''
                UPDATE cost_periods
                SET total_costs = ?,
                    active_issues_count = ?,
                    cost_per_issue = ?,
                    loss_carried_forward = ?,
                    updated_at = CURRENT_TIMESTAMP
                WHERE id = ?
            ''', (float(total_costs), active_issues_count, float(cost_per_issue), float(loss_carried_forward), period_id))