"""
Cost Item Management System for MarkiTect.

This module provides comprehensive cost item lifecycle management including:
- Cost item creation, modification, and lifecycle management
- Category management and validation
- Business rule enforcement and validation
- Integration with period management for cost calculations

The system supports both monthly recurring costs and one-time expenses
with proper validation and audit trails.
"""

import sqlite3
from datetime import date, datetime
from decimal import Decimal
from typing import Optional, List, Dict, Any, Union
from dataclasses import dataclass

from .models import FinanceModels


@dataclass
class CostItem:
    """Data class representing a cost item."""
    id: Optional[int] = None
    category_id: int = None
    name: str = ""
    description: Optional[str] = None
    cost_type: str = ""  # 'monthly' or 'one_time'
    amount_eur: Decimal = Decimal('0.00')
    currency: str = "EUR"
    starting_from_date: date = None
    ending_date: Optional[date] = None
    is_active: bool = True
    created_at: Optional[datetime] = None
    updated_at: Optional[datetime] = None


@dataclass
class CostCategory:
    """Data class representing a cost category."""
    id: Optional[int] = None
    name: str = ""
    description: Optional[str] = None
    created_at: Optional[datetime] = None
    updated_at: Optional[datetime] = None


class CostItemManager:
    """Manager for cost item operations and business logic."""

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

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

    def create_cost_item(self, cost_item: CostItem) -> Optional[int]:
        """
        Create a new cost item with validation.

        Args:
            cost_item: CostItem instance to create

        Returns:
            ID of created cost item, or None if creation failed

        Raises:
            ValueError: If validation fails
            sqlite3.Error: If database operation fails
        """
        # Validate cost item
        self._validate_cost_item(cost_item)

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

        try:
            cursor.execute('''
                INSERT INTO cost_items
                (category_id, name, description, cost_type, amount_eur, currency,
                 starting_from_date, ending_date, is_active, created_at, updated_at)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            ''', (
                cost_item.category_id,
                cost_item.name,
                cost_item.description,
                cost_item.cost_type,
                float(cost_item.amount_eur),
                cost_item.currency,
                cost_item.starting_from_date.isoformat() if cost_item.starting_from_date else None,
                cost_item.ending_date.isoformat() if cost_item.ending_date else None,
                cost_item.is_active,
                datetime.now().isoformat(),
                datetime.now().isoformat()
            ))

            cost_item_id = cursor.lastrowid
            conn.commit()

            return cost_item_id

        except sqlite3.Error as e:
            conn.rollback()
            raise

        finally:
            conn.close()

    def update_cost_item(self, cost_item_id: int, updates: Dict[str, Any]) -> bool:
        """
        Update an existing cost item.

        Args:
            cost_item_id: ID of cost item to update
            updates: Dictionary of field updates

        Returns:
            True if update was successful, False otherwise

        Raises:
            ValueError: If validation fails
            sqlite3.Error: If database operation fails
        """
        # Get existing cost item for validation
        existing_item = self.get_cost_item(cost_item_id)
        if not existing_item:
            raise ValueError(f"Cost item with ID {cost_item_id} not found")

        # Apply updates to existing item for validation
        # Filter out extra fields that aren't part of CostItem dataclass
        cost_item_fields = {
            'id', 'category_id', 'name', 'description', 'cost_type',
            'amount_eur', 'currency', 'starting_from_date', 'ending_date',
            'is_active', 'created_at', 'updated_at'
        }
        filtered_item = {k: v for k, v in existing_item.items() if k in cost_item_fields}

        # Convert date strings back to date objects for validation
        if filtered_item.get('starting_from_date'):
            filtered_item['starting_from_date'] = date.fromisoformat(filtered_item['starting_from_date'])
        if filtered_item.get('ending_date'):
            filtered_item['ending_date'] = date.fromisoformat(filtered_item['ending_date'])
        if filtered_item.get('amount_eur'):
            filtered_item['amount_eur'] = Decimal(str(filtered_item['amount_eur']))

        updated_item = CostItem(**filtered_item)
        for field, value in updates.items():
            if hasattr(updated_item, field):
                setattr(updated_item, field, value)
            else:
                raise ValueError(f"Invalid field: {field}")

        # Validate updated item
        self._validate_cost_item(updated_item)

        # Build dynamic update query
        set_clauses = []
        values = []

        for field, value in updates.items():
            if field in ['amount_eur'] and isinstance(value, Decimal):
                value = float(value)
            elif field in ['starting_from_date', 'ending_date'] and isinstance(value, date):
                value = value.isoformat() if value else None

            set_clauses.append(f"{field} = ?")
            values.append(value)

        # Add updated_at timestamp
        set_clauses.append("updated_at = ?")
        values.append(datetime.now().isoformat())
        values.append(cost_item_id)

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

        try:
            cursor.execute(f'''
                UPDATE cost_items
                SET {', '.join(set_clauses)}
                WHERE id = ?
            ''', values)

            success = cursor.rowcount > 0
            conn.commit()

            return success

        except sqlite3.Error as e:
            conn.rollback()
            raise

        finally:
            conn.close()

    def deactivate_cost_item(self, cost_item_id: int, ending_date: Optional[date] = None) -> bool:
        """
        Deactivate a cost item (soft delete).

        Args:
            cost_item_id: ID of cost item to deactivate
            ending_date: Optional ending date (defaults to today)

        Returns:
            True if deactivation was successful, False otherwise
        """
        if ending_date is None:
            ending_date = date.today()

        updates = {
            'is_active': False,
            'ending_date': ending_date
        }

        return self.update_cost_item(cost_item_id, updates)

    def get_cost_item(self, cost_item_id: int) -> Optional[Dict[str, Any]]:
        """
        Retrieve a cost item by ID.

        Args:
            cost_item_id: ID of cost item to retrieve

        Returns:
            Dictionary containing cost item data, or None if not found
        """
        conn = self.finance_models.get_connection()
        cursor = conn.cursor()

        try:
            cursor.execute('''
                SELECT ci.*, cc.name as category_name
                FROM cost_items ci
                LEFT JOIN cost_categories cc ON ci.category_id = cc.id
                WHERE ci.id = ?
            ''', (cost_item_id,))

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

            # Convert row to dictionary
            columns = [desc[0] for desc in cursor.description]
            return dict(zip(columns, row))

        except sqlite3.Error as e:
            return None

        finally:
            conn.close()

    def list_cost_items(self,
                       active_only: bool = True,
                       category_id: Optional[int] = None,
                       cost_type: Optional[str] = None) -> List[Dict[str, Any]]:
        """
        List cost items with optional filtering.

        Args:
            active_only: If True, only return active cost items
            category_id: Optional category filter
            cost_type: Optional cost type filter ('monthly' or 'one_time')

        Returns:
            List of cost item dictionaries
        """
        conn = self.finance_models.get_connection()
        cursor = conn.cursor()

        # Build WHERE clause
        conditions = []
        values = []

        if active_only:
            conditions.append("ci.is_active = ?")
            values.append(True)

        if category_id is not None:
            conditions.append("ci.category_id = ?")
            values.append(category_id)

        if cost_type is not None:
            conditions.append("ci.cost_type = ?")
            values.append(cost_type)

        where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""

        try:
            cursor.execute(f'''
                SELECT ci.*, cc.name as category_name
                FROM cost_items ci
                LEFT JOIN cost_categories cc ON ci.category_id = cc.id
                {where_clause}
                ORDER BY ci.category_id, ci.name
            ''', values)

            rows = cursor.fetchall()
            columns = [desc[0] for desc in cursor.description]

            return [dict(zip(columns, row)) for row in rows]

        except sqlite3.Error as e:
            return []

        finally:
            conn.close()

    def get_active_costs_for_period(self, period_start: date, period_end: date) -> List[Dict[str, Any]]:
        """
        Get all active cost items for a specific period.

        Args:
            period_start: Start date of the period
            period_end: End date of the period

        Returns:
            List of active cost items for the period
        """
        conn = self.finance_models.get_connection()
        cursor = conn.cursor()

        try:
            cursor.execute('''
                SELECT ci.*, cc.name as category_name
                FROM cost_items ci
                LEFT JOIN cost_categories cc ON ci.category_id = cc.id
                WHERE ci.is_active = TRUE
                  AND ci.starting_from_date <= ?
                  AND (ci.ending_date IS NULL OR ci.ending_date >= ?)
                ORDER BY ci.category_id, ci.name
            ''', (period_end.isoformat(), period_start.isoformat()))

            rows = cursor.fetchall()
            columns = [desc[0] for desc in cursor.description]

            return [dict(zip(columns, row)) for row in rows]

        except sqlite3.Error as e:
            return []

        finally:
            conn.close()

    def calculate_period_costs(self, period_start: date, period_end: date) -> Dict[str, Any]:
        """
        Calculate total costs for a specific period.

        Args:
            period_start: Start date of the period
            period_end: End date of the period

        Returns:
            Dictionary with cost calculations
        """
        active_costs = self.get_active_costs_for_period(period_start, period_end)

        total_monthly = Decimal('0.00')
        total_one_time = Decimal('0.00')
        category_totals = {}

        for cost_item in active_costs:
            amount = Decimal(str(cost_item['amount_eur']))
            cost_type = cost_item['cost_type']
            category = cost_item['category_name']

            if cost_type == 'monthly':
                total_monthly += amount
            elif cost_type == 'one_time':
                total_one_time += amount

            # Track by category
            if category not in category_totals:
                category_totals[category] = {'monthly': Decimal('0.00'), 'one_time': Decimal('0.00')}
            category_totals[category][cost_type] += amount

        return {
            'period_start': period_start,
            'period_end': period_end,
            'total_monthly': float(total_monthly),
            'total_one_time': float(total_one_time),
            'total_period': float(total_monthly + total_one_time),
            'category_breakdown': {
                cat: {
                    'monthly': float(amounts['monthly']),
                    'one_time': float(amounts['one_time']),
                    'total': float(amounts['monthly'] + amounts['one_time'])
                }
                for cat, amounts in category_totals.items()
            },
            'active_cost_items': len(active_costs)
        }

    def _validate_cost_item(self, cost_item: CostItem) -> None:
        """
        Validate cost item data.

        Args:
            cost_item: CostItem to validate

        Raises:
            ValueError: If validation fails
        """
        if not cost_item.name or not cost_item.name.strip():
            raise ValueError("Cost item name is required")

        if cost_item.cost_type not in ['monthly', 'one_time']:
            raise ValueError("Cost type must be 'monthly' or 'one_time'")

        if cost_item.amount_eur is None or cost_item.amount_eur < 0:
            raise ValueError("Amount must be non-negative")

        if not cost_item.starting_from_date:
            raise ValueError("Starting date is required")

        if cost_item.ending_date and cost_item.ending_date < cost_item.starting_from_date:
            raise ValueError("Ending date must be after starting date")

        if not cost_item.is_active and cost_item.ending_date is None:
            raise ValueError("Inactive cost items must have an ending date")

        # Validate category exists
        if cost_item.category_id:
            category = self.get_category(cost_item.category_id)
            if not category:
                raise ValueError(f"Category with ID {cost_item.category_id} does not exist")

    # Category management methods

    def create_category(self, name: str, description: Optional[str] = None) -> Optional[int]:
        """
        Create a new cost category.

        Args:
            name: Category name
            description: Optional category description

        Returns:
            ID of created category, or None if creation failed
        """
        if not name or not name.strip():
            raise ValueError("Category name is required")

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

        try:
            cursor.execute('''
                INSERT INTO cost_categories (name, description, created_at, updated_at)
                VALUES (?, ?, ?, ?)
            ''', (name.strip(), description, datetime.now().isoformat(), datetime.now().isoformat()))

            category_id = cursor.lastrowid
            conn.commit()

            return category_id

        except sqlite3.IntegrityError:
            conn.rollback()
            raise ValueError(f"Category '{name}' already exists")

        except sqlite3.Error as e:
            conn.rollback()
            raise

        finally:
            conn.close()

    def get_category(self, category_id: int) -> Optional[Dict[str, Any]]:
        """
        Retrieve a category by ID.

        Args:
            category_id: ID of category to retrieve

        Returns:
            Dictionary containing category data, or None if not found
        """
        conn = self.finance_models.get_connection()
        cursor = conn.cursor()

        try:
            cursor.execute('SELECT * FROM cost_categories WHERE id = ?', (category_id,))
            row = cursor.fetchone()

            if not row:
                return None

            columns = [desc[0] for desc in cursor.description]
            return dict(zip(columns, row))

        except sqlite3.Error as e:
            return None

        finally:
            conn.close()

    def list_categories(self) -> List[Dict[str, Any]]:
        """
        List all cost categories.

        Returns:
            List of category dictionaries
        """
        conn = self.finance_models.get_connection()
        cursor = conn.cursor()

        try:
            cursor.execute('SELECT * FROM cost_categories ORDER BY name')
            rows = cursor.fetchall()
            columns = [desc[0] for desc in cursor.description]

            return [dict(zip(columns, row)) for row in rows]

        except sqlite3.Error as e:
            return []

        finally:
            conn.close()

    def get_category_by_name(self, name: str) -> Optional[Dict[str, Any]]:
        """
        Retrieve a category by name.

        Args:
            name: Name of category to retrieve

        Returns:
            Dictionary containing category data, or None if not found
        """
        conn = self.finance_models.get_connection()
        cursor = conn.cursor()

        try:
            cursor.execute('SELECT * FROM cost_categories WHERE name = ?', (name,))
            row = cursor.fetchone()

            if not row:
                return None

            columns = [desc[0] for desc in cursor.description]
            return dict(zip(columns, row))

        except sqlite3.Error as e:
            return None

        finally:
            conn.close()