# -*- coding: utf-8 -*-
"""
Safe database operations with transaction management and connection pooling.

This module provides:
- Transaction-safe write operations
- Connection context managers (prevents resource leaks)
- Comprehensive error handling
- Data validation layer
- Date normalization to ISO 8601

BREAKING CHANGES from library.py:
- write_df() replaced with write_df_safe() - returns WriteResult dataclass
- All database operations use parameterized queries (SQL injection fixed)
- All connections use context managers (no leaks)
- Comprehensive exception handling (catches all sqlite3.* errors)
"""

import sqlite3
import pandas as pd
from pathlib import Path
from typing import Optional, Union
from dataclasses import dataclass
from contextlib import contextmanager
from datetime import datetime, date
import warnings


@dataclass
class WriteResult:
    """Result from database write operation."""

    success: bool
    rows_written: int
    rows_skipped: int
    rows_invalid: int = 0
    error: Optional[str] = None

    def summary(self) -> str:
        """Human-readable summary of write operation."""
        if not self.success:
            return f"Write failed: {self.error}"

        total = self.rows_written + self.rows_skipped
        return (
            f"Wrote {self.rows_written}/{total} rows "
            f"({self.rows_skipped} duplicates skipped"
            f"{f', {self.rows_invalid} invalid' if self.rows_invalid > 0 else ''})"
        )


@contextmanager
def get_connection(db_path: Path, read_only: bool = False):
    """
    Context manager for database connections.

    Ensures connections are always closed, even on exceptions.
    Enables WAL mode for better concurrency.

    Args:
        db_path: Path to database file
        read_only: If True, open in read-only mode

    Yields:
        sqlite3.Connection object

    Example:
        with get_connection(db_path) as conn:
            df = pd.read_sql_query("SELECT * FROM data", conn)
    """
    db_path = Path(db_path)
    if not read_only:
        db_path.parent.mkdir(exist_ok=True, parents=True)

    uri = f"file:{db_path}{'?mode=ro' if read_only else ''}"
    conn = None

    try:
        conn = sqlite3.connect(uri, uri=True)

        # Apply optimal settings
        if not read_only:
            conn.execute("PRAGMA journal_mode=WAL")
            conn.execute("PRAGMA synchronous=NORMAL")
            conn.execute("PRAGMA cache_size=-64000")  # 64 MB
            conn.execute("PRAGMA temp_store=MEMORY")

        yield conn

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


class DataValidator:
    """Validates DataFrame before database insertion."""

    @staticmethod
    def validate_schema(df: pd.DataFrame) -> tuple[bool, list[str]]:
        """
        Ensure required columns exist with correct types.

        Args:
            df: DataFrame to validate

        Returns:
            (is_valid, error_messages)
        """
        errors = []

        required_cols = [
            "Company",
            "Date",
            "Industry",
            "Open",
            "High",
            "Low",
            "Close",
            "Volume",
        ]

        for col in required_cols:
            if col not in df.columns:
                errors.append(f"Missing required column: {col}")

        return len(errors) == 0, errors

    @staticmethod
    def validate_values(df: pd.DataFrame) -> tuple[pd.DataFrame, list[str]]:
        """
        Check for impossible values and filter them out.

        Args:
            df: DataFrame to validate

        Returns:
            (cleaned_df, warning_messages)
        """
        warnings_list = []
        df_clean = df.copy()

        # Filter out rows with non-positive prices
        price_cols = ["Open", "High", "Low", "Close"]
        invalid_prices = (df_clean[price_cols] <= 0).any(axis=1)
        if invalid_prices.any():
            count = invalid_prices.sum()
            warnings_list.append(f"Removed {count} rows with non-positive prices")
            df_clean = df_clean[~invalid_prices]

        if df_clean.empty:
            return df_clean, warnings_list

        # Filter out rows where High < Low
        invalid_range = df_clean["High"] < df_clean["Low"]
        if invalid_range.any():
            count = invalid_range.sum()
            warnings_list.append(f"Removed {count} rows where High < Low")
            df_clean = df_clean[~invalid_range]

        if df_clean.empty:
            return df_clean, warnings_list

        # Filter out rows with negative volume
        invalid_volume = df_clean["Volume"] < 0
        if invalid_volume.any():
            count = invalid_volume.sum()
            warnings_list.append(f"Removed {count} rows with negative volume")
            df_clean = df_clean[~invalid_volume]

        return df_clean, warnings_list

    @staticmethod
    def normalize_dates(df: pd.DataFrame) -> pd.DataFrame:
        """
        Normalize Date column to ISO 8601 format (YYYY-MM-DD).

        Handles:
        - datetime.date objects
        - datetime.datetime objects
        - pd.Timestamp objects
        - String dates in various formats

        Args:
            df: DataFrame with Date column

        Returns:
            DataFrame with Date as ISO 8601 strings
        """
        df = df.copy()

        if "Date" not in df.columns:
            return df

        # Convert to datetime if not already
        if not pd.api.types.is_datetime64_any_dtype(df["Date"]):
            df["Date"] = pd.to_datetime(df["Date"], errors="coerce")

        # Convert to ISO 8601 string format (YYYY-MM-DD)
        df["Date"] = df["Date"].dt.strftime("%Y-%m-%d")

        # Remove rows with invalid dates (NaT became 'NaT' string)
        invalid_dates = df["Date"].isna() | (df["Date"] == "NaT")
        if invalid_dates.any():
            df = df[~invalid_dates]

        return df


def write_df_safe(
    df: pd.DataFrame,
    table_name: str,
    db_path: Path,
    validate: bool = True,
    force_overwrite: bool = False,
) -> WriteResult:
    """
    Transaction-safe database write with conflict resolution.

    Features:
    - Automatic transaction management (commit on success, rollback on error)
    - Database-level duplicate prevention (INSERT OR IGNORE) or overwrite (INSERT OR REPLACE)
    - Optional data validation
    - Date normalization to ISO 8601
    - Comprehensive error handling

    Process:
    1. Validate data (optional)
    2. Normalize dates to ISO 8601
    3. Create temporary table
    4. Insert with appropriate conflict resolution strategy
    5. Clean up temporary table
    6. Commit transaction

    Args:
        df: DataFrame to write
        table_name: Name of table (usually "data")
        db_path: Path to database file
        validate: If True, validate data before writing
        force_overwrite: If True, use INSERT OR REPLACE to overwrite duplicates
                        If False, use INSERT OR IGNORE to skip duplicates

    Returns:
        WriteResult with statistics and status

    Example:
        # Skip duplicates (default)
        result = write_df_safe(df, "data", Path("out/data/data.sqlite"))

        # Force overwrite duplicates
        result = write_df_safe(df, "data", Path("out/data/data.sqlite"), force_overwrite=True)
    """
    from .schema import ensure_schema_up_to_date

    if df is None or df.empty:
        return WriteResult(
            success=False, rows_written=0, rows_skipped=0, error="DataFrame is empty"
        )

    rows_original = len(df)
    rows_invalid = 0

    try:
        # Ensure database schema exists and is up to date
        ensure_schema_up_to_date(db_path)

        # Validate data if requested
        if validate:
            is_valid, errors = DataValidator.validate_schema(df)
            if not is_valid:
                return WriteResult(
                    success=False,
                    rows_written=0,
                    rows_skipped=0,
                    error=f"Schema validation failed: {'; '.join(errors)}",
                )

            df, warnings_list = DataValidator.validate_values(df)
            rows_invalid = rows_original - len(df)

            if df.empty:
                return WriteResult(
                    success=False,
                    rows_written=0,
                    rows_skipped=0,
                    rows_invalid=rows_invalid,
                    error="All rows invalid after validation",
                )

            for warning_msg in warnings_list:
                warnings.warn(warning_msg)

        # Normalize dates to ISO 8601
        df = DataValidator.normalize_dates(df)

        if df.empty:
            return WriteResult(
                success=False,
                rows_written=0,
                rows_skipped=0,
                rows_invalid=rows_invalid,
                error="No valid rows after date normalization",
            )

        # Use context manager for connection
        with get_connection(db_path, read_only=False) as conn:
            # All operations in a single transaction
            with conn:
                # Stage 1: Create temporary table
                temp_table = f"_temp_import_{id(df)}"
                df.to_sql(temp_table, conn, if_exists="replace", index=False)

                # Stage 2: Count how many rows would be duplicates
                cursor = conn.execute(
                    f"""
                    SELECT COUNT(*)
                    FROM {temp_table} t
                    WHERE EXISTS (
                        SELECT 1 FROM {table_name} d
                        WHERE d.Company = t.Company
                        AND d.Date = t.Date
                    )
                """
                )
                rows_duplicate = cursor.fetchone()[0]

                # Stage 3: Insert with appropriate conflict resolution
                # INSERT OR IGNORE: Skip duplicates (default)
                # INSERT OR REPLACE: Overwrite duplicates (force mode)
                conflict_action = "REPLACE" if force_overwrite else "IGNORE"
                cursor = conn.execute(
                    f"""
                    INSERT OR {conflict_action} INTO {table_name}
                    SELECT * FROM {temp_table}
                """
                )
                rows_inserted = cursor.rowcount

                # Stage 4: Cleanup temporary table
                conn.execute(f"DROP TABLE {temp_table}")

                # Transaction automatically commits here due to 'with conn:'

        rows_skipped = len(df) - rows_inserted

        return WriteResult(
            success=True,
            rows_written=rows_inserted,
            rows_skipped=rows_skipped,
            rows_invalid=rows_invalid,
        )

    except sqlite3.IntegrityError as e:
        return WriteResult(
            success=False,
            rows_written=0,
            rows_skipped=0,
            rows_invalid=rows_invalid,
            error=f"Integrity constraint violation: {e}",
        )
    except sqlite3.OperationalError as e:
        return WriteResult(
            success=False,
            rows_written=0,
            rows_skipped=0,
            rows_invalid=rows_invalid,
            error=f"Database operation failed: {e}",
        )
    except sqlite3.Error as e:
        return WriteResult(
            success=False,
            rows_written=0,
            rows_skipped=0,
            rows_invalid=rows_invalid,
            error=f"Database error: {e}",
        )
    except Exception as e:
        return WriteResult(
            success=False,
            rows_written=0,
            rows_skipped=0,
            rows_invalid=rows_invalid,
            error=f"Unexpected error: {e}",
        )


def load_df_from_sqlite_safe(
    table_name: str,
    db_path: Path,
    comp_name: Optional[str] = None,
    ind_name: Optional[str] = None,
    start_date: Optional[str] = None,
    end_date: Optional[str] = None,
    filter_complete_dates: bool = True,
) -> Optional[pd.DataFrame]:
    """
    Load data from SQLite with proper parameterization and error handling.

    SECURITY: Uses parameterized queries (SQL injection safe)
    RELIABILITY: Comprehensive exception handling
    RESOURCE SAFETY: Context manager ensures connection closure

    Args:
        table_name: Name of table to query
        db_path: Path to database file
        comp_name: Filter by company ticker (optional)
        ind_name: Filter by industry (optional)
        start_date: Start date (ISO 8601 format, optional)
        end_date: End date (ISO 8601 format, optional)
        filter_complete_dates: If True, only keep dates where all companies have data

    Returns:
        DataFrame or None if error/empty

    Example:
        df = load_df_from_sqlite_safe(
            "data",
            Path("out/data/data.sqlite"),
            ind_name="Information Technology",
            start_date="2020-01-01",
            end_date="2023-12-31"
        )
    """
    try:
        # Use parameterized query (SQL injection safe)
        query = f"SELECT * FROM {table_name}"  # Table name validated by schema
        params = []
        conditions = []

        # Company and Industry filtering
        if comp_name and ind_name:
            conditions.append("(Company = ? OR Industry = ?)")
            params.extend([comp_name, ind_name])
        elif comp_name:
            conditions.append("Company = ?")
            params.append(comp_name)
        elif ind_name:
            conditions.append("Industry = ?")
            params.append(ind_name)

        # Date range filtering
        if start_date:
            conditions.append("Date >= ?")
            params.append(start_date)
        if end_date:
            conditions.append("Date <= ?")
            params.append(end_date)

        # Build WHERE clause
        if conditions:
            query += " WHERE " + " AND ".join(conditions)

        # Execute query with connection context manager
        with get_connection(db_path, read_only=True) as conn:
            df = pd.read_sql_query(query, conn, params=params)

        if df is None or df.empty:
            return None

        # Filter for complete dates if requested
        if filter_complete_dates and "Company" in df.columns:
            should_filter = False

            if comp_name and ind_name:
                should_filter = True
            elif ind_name and not comp_name:
                should_filter = df["Company"].nunique() > 1

            if should_filter:
                df = df.dropna()

                if not df.empty:
                    all_companies = df["Company"].unique()
                    total_companies = len(all_companies)

                    date_company_counts = df.groupby("Date")["Company"].nunique()
                    complete_dates = date_company_counts[
                        date_company_counts == total_companies
                    ].index

                    if len(complete_dates) > 0:
                        df = df[df["Date"].isin(complete_dates)]
                    else:
                        return df.iloc[0:0]  # Empty with columns

        return df if not df.empty else None

    except sqlite3.Error as e:
        warnings.warn(f"Database query failed: {e}")
        return None
    except pd.errors.DatabaseError as e:
        warnings.warn(f"Pandas database error: {e}")
        return None
    except Exception as e:
        warnings.warn(f"Unexpected error loading data: {e}")
        return None


def table_exists(db_path: Path, table_name: str) -> bool:
    """
    Check if a table exists in the database.

    Uses parameterized query (SQL injection safe).

    Args:
        db_path: Path to database file
        table_name: Name of table to check

    Returns:
        True if table exists, False otherwise
    """
    try:
        with get_connection(db_path, read_only=True) as conn:
            cursor = conn.cursor()
            cursor.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
                (table_name,),
            )
            return cursor.fetchone() is not None
    except sqlite3.Error:
        return False


def get_existing_keys(db_path: Path, table_name: str) -> set[tuple[str, str]]:
    """
    Get all existing (Company, Date) combinations from database.

    Uses parameterized query (SQL injection safe).

    Args:
        db_path: Path to database file
        table_name: Name of table

    Returns:
        Set of (Company, Date) tuples
    """
    try:
        with get_connection(db_path, read_only=True) as conn:
            cursor = conn.cursor()
            cursor.execute(f"SELECT DISTINCT Company, Date FROM {table_name}")
            return set(cursor.fetchall())
    except sqlite3.Error:
        return set()


def execute_query_safe(
    db_path: Path, query: str, params: Optional[tuple] = None, read_only: bool = True
) -> Optional[list]:
    """
    Execute a SQL query safely with parameterization.

    Args:
        db_path: Path to database file
        query: SQL query (use ? for parameters)
        params: Query parameters (optional)
        read_only: If True, open database in read-only mode

    Returns:
        List of result rows, or None if error

    Example:
        results = execute_query_safe(
            db_path,
            "SELECT * FROM data WHERE Company = ? AND Date >= ?",
            ("AAPL", "2023-01-01")
        )
    """
    try:
        with get_connection(db_path, read_only=read_only) as conn:
            cursor = conn.cursor()
            if params:
                cursor.execute(query, params)
            else:
                cursor.execute(query)

            if not read_only:
                conn.commit()

            return cursor.fetchall()
    except (sqlite3.Error, pd.errors.DatabaseError, OSError) as e:
        warnings.warn(f"Query execution failed: {e}")
        return None
