"""
Period Management Framework for MarkiTect Cost Tracking System.

This module provides comprehensive period lifecycle management including:
- Period creation, status management, and transitions
- Period overlap validation and conflict resolution
- Automatic period calculations and data aggregation
- Loss carried forward calculations
- Period closure validation and audit trails

The framework supports the complete period lifecycle:
1. OPEN: Period is created and active for cost tracking
2. CALCULATING: Period is being processed for cost allocation
3. CLOSED: Period is finalized with all calculations complete
"""

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

from .models import FinanceModels


class PeriodStatus(Enum):
    """Period status enumeration."""
    OPEN = 'open'
    CALCULATING = 'calculating'
    CLOSED = 'closed'


@dataclass
class Period:
    """Period data model."""
    id: Optional[int] = None
    period_start: Optional[date] = None
    period_end: Optional[date] = None
    period_type: str = 'monthly'
    status: str = 'open'
    total_costs: Decimal = Decimal('0.00')
    active_issues_count: int = 0
    cost_per_issue: Decimal = Decimal('0.00')
    loss_carried_forward: Decimal = Decimal('0.00')
    created_at: Optional[datetime] = None
    updated_at: Optional[datetime] = None


class PeriodManager:
    """
    Comprehensive period management system for cost tracking.

    Handles period lifecycle management, status transitions, calculations,
    and validation for the cost tracking system.
    """

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

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

    def create_period(self, period_start: date, period_end: date,
                     period_type: str = 'monthly',
                     loss_carried_forward: Optional[Decimal] = None) -> Optional[int]:
        """
        Create a new calculation period with validation.

        Args:
            period_start: Start date of the period
            period_end: End date of the period
            period_type: Type of period (monthly, quarterly, yearly)
            loss_carried_forward: Amount carried forward from previous period

        Returns:
            ID of created period, or None if creation failed

        Raises:
            ValueError: If period dates are invalid or overlapping periods exist
        """
        # Validate period dates
        if period_end <= period_start:
            raise ValueError("Period end date must be after start date")

        # Check for overlapping periods
        overlapping = self.find_overlapping_periods(period_start, period_end)
        if overlapping:
            overlapping_periods = [f"#{p['id']} ({p['period_start']} to {p['period_end']})"
                                 for p in overlapping]
            raise ValueError(f"Period overlaps with existing periods: {', '.join(overlapping_periods)}")

        # Set loss carried forward to 0 if not provided
        if loss_carried_forward is None:
            loss_carried_forward = Decimal('0.00')

        conn = self.finance_models.get_connection()
        cursor = conn.cursor()

        try:
            cursor.execute('''
                INSERT INTO cost_periods (
                    period_start, period_end, period_type, status,
                    total_costs, active_issues_count, cost_per_issue,
                    loss_carried_forward
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
            ''', (
                period_start.isoformat(),
                period_end.isoformat(),
                period_type,
                PeriodStatus.OPEN.value,
                0,
                0,
                0,
                float(loss_carried_forward)
            ))

            conn.commit()
            period_id = cursor.lastrowid
            return period_id

        except sqlite3.Error as e:
            conn.rollback()
            raise RuntimeError(f"Failed to create period: {e}")

        finally:
            conn.close()

    def get_period_by_id(self, period_id: int) -> Optional[Dict[str, Any]]:
        """
        Get period by ID.

        Args:
            period_id: Period ID

        Returns:
            Period dictionary or None if not found
        """
        conn = self.finance_models.get_connection()
        cursor = conn.cursor()

        try:
            cursor.execute('''
                SELECT id, period_start, period_end, period_type, status,
                       total_costs, active_issues_count, cost_per_issue,
                       loss_carried_forward, created_at, updated_at
                FROM cost_periods
                WHERE id = ?
            ''', (period_id,))

            row = cursor.fetchone()
            if not row:
                return None

            return {
                'id': row[0],
                'period_start': row[1],
                'period_end': row[2],
                'period_type': row[3],
                'status': row[4],
                'total_costs': Decimal(str(row[5])),
                'active_issues_count': row[6],
                'cost_per_issue': Decimal(str(row[7])),
                'loss_carried_forward': Decimal(str(row[8])),
                'created_at': row[9],
                'updated_at': row[10]
            }

        except sqlite3.Error as e:
            raise RuntimeError(f"Failed to get period: {e}")

        finally:
            conn.close()

    def find_overlapping_periods(self, period_start: date, period_end: date,
                                exclude_period_id: Optional[int] = None) -> List[Dict[str, Any]]:
        """
        Find periods that overlap with the given date range.

        Args:
            period_start: Start date to check
            period_end: End date to check
            exclude_period_id: Period ID to exclude from check (for updates)

        Returns:
            List of overlapping periods
        """
        conn = self.finance_models.get_connection()
        cursor = conn.cursor()

        try:
            sql = '''
                SELECT id, period_start, period_end, status
                FROM cost_periods
                WHERE NOT (period_end < ? OR period_start > ?)
            '''
            params = [period_start.isoformat(), period_end.isoformat()]

            if exclude_period_id:
                sql += ' AND id != ?'
                params.append(exclude_period_id)

            cursor.execute(sql, params)
            rows = cursor.fetchall()

            return [
                {
                    'id': row[0],
                    'period_start': row[1],
                    'period_end': row[2],
                    'status': row[3]
                }
                for row in rows
            ]

        except sqlite3.Error as e:
            raise RuntimeError(f"Failed to check overlapping periods: {e}")

        finally:
            conn.close()

    def update_period_status(self, period_id: int, new_status: str) -> bool:
        """
        Update period status with validation.

        Args:
            period_id: Period ID to update
            new_status: New status (open, calculating, closed)

        Returns:
            True if update successful, False otherwise

        Raises:
            ValueError: If status transition is invalid
        """
        valid_statuses = [status.value for status in PeriodStatus]
        if new_status not in valid_statuses:
            raise ValueError(f"Invalid status '{new_status}'. Valid statuses: {valid_statuses}")

        # Get current period to validate status transition
        current_period = self.get_period_by_id(period_id)
        if not current_period:
            raise ValueError(f"Period #{period_id} not found")

        current_status = current_period['status']

        # Validate status transitions
        valid_transitions = {
            PeriodStatus.OPEN.value: [PeriodStatus.CALCULATING.value, PeriodStatus.CLOSED.value],
            PeriodStatus.CALCULATING.value: [PeriodStatus.OPEN.value, PeriodStatus.CLOSED.value],
            PeriodStatus.CLOSED.value: [PeriodStatus.OPEN.value]  # Allow reopening if needed
        }

        if new_status not in valid_transitions.get(current_status, []):
            raise ValueError(f"Invalid status transition from '{current_status}' to '{new_status}'")

        conn = self.finance_models.get_connection()
        cursor = conn.cursor()

        try:
            cursor.execute('''
                UPDATE cost_periods
                SET status = ?, updated_at = CURRENT_TIMESTAMP
                WHERE id = ?
            ''', (new_status, period_id))

            conn.commit()
            return cursor.rowcount > 0

        except sqlite3.Error as e:
            conn.rollback()
            raise RuntimeError(f"Failed to update period status: {e}")

        finally:
            conn.close()

    def calculate_period_costs(self, period_id: int) -> Dict[str, Any]:
        """
        Calculate total costs for a period based on active cost items.

        Args:
            period_id: Period ID to calculate

        Returns:
            Dictionary with calculation results
        """
        period = self.get_period_by_id(period_id)
        if not period:
            raise ValueError(f"Period #{period_id} not found")

        period_start = datetime.strptime(period['period_start'], '%Y-%m-%d').date()
        period_end = datetime.strptime(period['period_end'], '%Y-%m-%d').date()

        # Import CostItemManager to get cost data
        from .cost_manager import CostItemManager
        cost_manager = CostItemManager(self.db_path)

        # Get all active cost items
        cost_items = cost_manager.list_cost_items(active_only=True)

        total_monthly = Decimal('0')
        total_one_time = Decimal('0')

        for item in cost_items:
            item_start = datetime.strptime(item['starting_from_date'], '%Y-%m-%d').date()

            # Skip items that start after our period
            if item_start > period_end:
                continue

            # Handle ending date
            item_end = None
            if item['ending_date']:
                item_end = datetime.strptime(item['ending_date'], '%Y-%m-%d').date()
                # Skip items that ended before our period
                if item_end < period_start:
                    continue

            # Calculate cost for this period
            if item['cost_type'] == 'monthly':
                # For monthly items, include full amount if active during period
                total_monthly += Decimal(str(item['amount_eur']))
            elif item['cost_type'] == 'one_time':
                # For one-time items, include only if starting date is within period
                if period_start <= item_start <= period_end:
                    total_one_time += Decimal(str(item['amount_eur']))

        total_costs = total_monthly + total_one_time + period['loss_carried_forward']

        # Update period with calculated values
        conn = self.finance_models.get_connection()
        cursor = conn.cursor()

        try:
            cursor.execute('''
                UPDATE cost_periods
                SET total_costs = ?, updated_at = CURRENT_TIMESTAMP
                WHERE id = ?
            ''', (float(total_costs), period_id))

            conn.commit()

        except sqlite3.Error as e:
            conn.rollback()
            raise RuntimeError(f"Failed to update period calculations: {e}")

        finally:
            conn.close()

        return {
            'period_id': period_id,
            'period_start': period['period_start'],
            'period_end': period['period_end'],
            'monthly_costs': float(total_monthly),
            'one_time_costs': float(total_one_time),
            'loss_carried_forward': float(period['loss_carried_forward']),
            'total_costs': float(total_costs),
            'calculation_date': datetime.now().isoformat()
        }

    def close_period(self, period_id: int, validate_transactions: bool = True) -> bool:
        """
        Close a period after validation.

        Args:
            period_id: Period ID to close
            validate_transactions: Whether to validate all transactions are recorded

        Returns:
            True if period closed successfully, False otherwise

        Raises:
            ValueError: If period cannot be closed due to validation errors
        """
        period = self.get_period_by_id(period_id)
        if not period:
            raise ValueError(f"Period #{period_id} not found")

        if period['status'] == PeriodStatus.CLOSED.value:
            return True  # Already closed

        # Optional transaction validation
        if validate_transactions:
            # Check if there are any unrecorded transactions
            # This is a placeholder for future transaction validation logic
            pass

        # Calculate final costs before closing
        calculation_result = self.calculate_period_costs(period_id)

        # Update status to closed
        return self.update_period_status(period_id, PeriodStatus.CLOSED.value)

    def list_periods(self, status_filter: Optional[str] = None,
                    start_date: Optional[date] = None,
                    end_date: Optional[date] = None) -> List[Dict[str, Any]]:
        """
        List periods with optional filtering.

        Args:
            status_filter: Filter by status (open, calculating, closed)
            start_date: Filter periods starting from this date
            end_date: Filter periods ending before this date

        Returns:
            List of periods matching criteria
        """
        conn = self.finance_models.get_connection()
        cursor = conn.cursor()

        try:
            sql = '''
                SELECT id, period_start, period_end, period_type, status,
                       total_costs, active_issues_count, cost_per_issue,
                       loss_carried_forward, created_at, updated_at
                FROM cost_periods
                WHERE 1=1
            '''
            params = []

            if status_filter:
                sql += ' AND status = ?'
                params.append(status_filter)

            if start_date:
                sql += ' AND period_start >= ?'
                params.append(start_date.isoformat())

            if end_date:
                sql += ' AND period_end <= ?'
                params.append(end_date.isoformat())

            sql += ' ORDER BY period_start DESC'

            cursor.execute(sql, params)
            rows = cursor.fetchall()

            return [
                {
                    'id': row[0],
                    'period_start': row[1],
                    'period_end': row[2],
                    'period_type': row[3],
                    'status': row[4],
                    'total_costs': Decimal(str(row[5])),
                    'active_issues_count': row[6],
                    'cost_per_issue': Decimal(str(row[7])),
                    'loss_carried_forward': Decimal(str(row[8])),
                    'created_at': row[9],
                    'updated_at': row[10]
                }
                for row in rows
            ]

        except sqlite3.Error as e:
            raise RuntimeError(f"Failed to list periods: {e}")

        finally:
            conn.close()

    def get_current_period(self, target_date: Optional[date] = None) -> Optional[Dict[str, Any]]:
        """
        Get the current active period for a given date.

        Args:
            target_date: Date to find period for (defaults to today)

        Returns:
            Current period dictionary or None if no period found
        """
        if target_date is None:
            target_date = date.today()

        conn = self.finance_models.get_connection()
        cursor = conn.cursor()

        try:
            cursor.execute('''
                SELECT id, period_start, period_end, period_type, status,
                       total_costs, active_issues_count, cost_per_issue,
                       loss_carried_forward, created_at, updated_at
                FROM cost_periods
                WHERE period_start <= ? AND period_end >= ?
                AND status IN ('open', 'calculating')
                ORDER BY period_start DESC
                LIMIT 1
            ''', (target_date.isoformat(), target_date.isoformat()))

            row = cursor.fetchone()
            if not row:
                return None

            return {
                'id': row[0],
                'period_start': row[1],
                'period_end': row[2],
                'period_type': row[3],
                'status': row[4],
                'total_costs': Decimal(str(row[5])),
                'active_issues_count': row[6],
                'cost_per_issue': Decimal(str(row[7])),
                'loss_carried_forward': Decimal(str(row[8])),
                'created_at': row[9],
                'updated_at': row[10]
            }

        except sqlite3.Error as e:
            raise RuntimeError(f"Failed to get current period: {e}")

        finally:
            conn.close()

    def create_monthly_period(self, year: int, month: int) -> Optional[int]:
        """
        Convenience method to create a monthly period.

        Args:
            year: Year of the period
            month: Month of the period (1-12)

        Returns:
            ID of created period, or None if creation failed
        """
        # Calculate period dates
        period_start = date(year, month, 1)

        # Calculate last day of month
        if month == 12:
            next_month_start = date(year + 1, 1, 1)
        else:
            next_month_start = date(year, month + 1, 1)

        period_end = next_month_start - timedelta(days=1)

        return self.create_period(period_start, period_end, 'monthly')

    def auto_create_period_for_date(self, target_date: date) -> Optional[int]:
        """
        Automatically create a monthly period for a given date if it doesn't exist.

        Args:
            target_date: Date that should be covered by a period

        Returns:
            ID of existing or newly created period, or None if creation failed
        """
        # Check if period already exists
        current_period = self.get_current_period(target_date)
        if current_period:
            return current_period['id']

        # Create new monthly period for the date
        year = target_date.year
        month = target_date.month

        try:
            return self.create_monthly_period(year, month)
        except ValueError as e:
            # Period might already exist due to race condition, try to get it again
            current_period = self.get_current_period(target_date)
            if current_period:
                return current_period['id']
            raise e