import json
import uuid
from datetime import date, datetime
from decimal import Decimal
from typing import Optional
from sqlalchemy import create_engine, MetaData, Table, select, insert, update, text, and_, or_
from sqlalchemy.orm import Session
from sqlalchemy.engine import Engine
from typing import Any
import pandas_market_calendars as mcal
import datetime as datetime
from datetime import datetime as dt, timedelta
from more_itertools import windowed
from bisect import  bisect_right
import pandas as pd

from io import BytesIO
import base64
import requests
import copy

import re
import numpy as np
from scipy.stats import norm
from enum import Enum
import bisect

from dotenv import load_dotenv
import os
import math

import datetime as datetime
from datetime import datetime as dt

import pandas as pd

from dotenv import load_dotenv
from io import BytesIO



import pandas as pd


import os



from datetime import datetime, date


load_dotenv()
server = os.getenv("SQL_SERVER")
user = os.getenv("SQL_USER")
password = os.getenv("SQL_PASSWORD")
database = os.getenv("SQL_DATABASE")

class SQLHandler:
    def __init__(self, server: str, user: str, password: str, database: str):
        if not all([server, user, password, database]):
            raise ValueError("Database credentials not fully provided.")

        driver = "ODBC Driver 17 for SQL Server"
        connection_string = (
            f"mssql+pyodbc://{user}:{password}@{server}/{database}"
            f"?driver={driver.replace(' ', '+')}"
        )

        self.engine: Engine = create_engine(connection_string, future=True)
        self.metadata = MetaData()
        self.metadata.reflect(bind=self.engine)
    
    def upsert_bulk(self, data: list[dict], table, id_column: str) -> list[str]:
        if not data:
            return []

        valid_columns = {c.name for c in table.columns}
        timestamp_columns = {"new_createdon"}
        skip_update_cols = {"new_createdby"}
        cleaned_data = []

        # Normalize and validate
        for record in data:
            record = record.copy()

            if "entity_id" in record:
                record[id_column] = record.pop("entity_id")

            row = {k: v for k, v in record.items() if k in valid_columns and k not in timestamp_columns}   
            if id_column not in row or not row[id_column]:
                row[id_column] = str(uuid.uuid4())

            for col in valid_columns - timestamp_columns:
                if col not in row:
                    row[col] = None

            cleaned_data.append(row)

        ids = [str(row[id_column]) for row in cleaned_data]
        cols = [c for c in valid_columns if c not in timestamp_columns]

        # Prepare column strings
        insert_cols = ", ".join(f"[{c}]" for c in cols)
        insert_vals = ", ".join(f":{c}" for c in cols)

        with Session(self.engine) as session:
            connection = session.connection()

            temp_table = f"#TempUpsert_{uuid.uuid4().hex[:8]}"
            connection.execute(
                text(f"SELECT TOP 0 * INTO {temp_table} FROM [dbo].[{table.name}]")
            )

            insert_stmt = text(f"INSERT INTO {temp_table} ({insert_cols}) VALUES ({insert_vals})")
            raw_conn = connection.connection
            cursor = raw_conn.cursor()
            cursor.fast_executemany = True
            cursor.executemany(
                f"INSERT INTO {temp_table} ({insert_cols}) VALUES ({', '.join(['?'] * len(cols))})",
                [[row[c] for c in cols] for row in cleaned_data],
            )

            set_clause = ", ".join(
            f"T.[{c}] = S.[{c}]"
            for c in cols
            if c not in skip_update_cols and c != id_column
            )
            update_stmt = f"""
                UPDATE T
                SET {set_clause}
                FROM [dbo].[{table.name}] AS T
                INNER JOIN {temp_table} AS S
                ON T.[{id_column}] = S.[{id_column}];
            """
            connection.execute(text(update_stmt))

            insert_stmt2 = f"""
                INSERT INTO [dbo].[{table.name}] ({insert_cols})
                SELECT {insert_cols}
                FROM {temp_table} AS S
                WHERE NOT EXISTS (
                    SELECT 1 FROM [dbo].[{table.name}] AS T
                    WHERE T.[{id_column}] = S.[{id_column}]
                );
            """
            connection.execute(text(insert_stmt2))

            connection.execute(text(f"DROP TABLE {temp_table}"))
            session.commit()

        return ids

    def get_entities(
        self,
        table,
        select_columns: str | None = None,
        filters: dict[str, object] | list[dict[str, object]] | None = None,
    ):
        with self.engine.connect() as conn:
            # Columns
            if select_columns:
                cols = []
                for name in (c.strip() for c in select_columns.split(",")):
                    if not name:
                        continue
                    if name not in table.c:
                        raise ValueError(
                            f"Column '{name}' not found in table '{table.name}'"
                        )
                    cols.append(table.c[name])
                stmt = select(*cols)
            else:
                stmt = select(table)

            # Filters (safe)
            if filters:
                def _build_filter_group(group: dict[str, object]):
                    if not isinstance(group, dict):
                        raise ValueError("Each filter group must be a dict of column->value")

                    exprs = []
                    for key, value in group.items():
                        if key not in table.c:
                            raise ValueError(
                                f"Filter column '{key}' not found in table '{table.name}'"
                            )
                        col = table.c[key]
                        if value is None:
                            exprs.append(col.is_(None))
                        elif isinstance(value, (list, tuple, set)):
                            exprs.append(col.in_(list(value)))
                        else:
                            exprs.append(col == value)
                    return and_(*exprs) if exprs else None

                if isinstance(filters, list):
                    groups = [_build_filter_group(g) for g in filters]
                    groups = [g for g in groups if g is not None]
                    if groups:
                        stmt = stmt.where(or_(*groups))
                else:
                    group_expr = _build_filter_group(filters)
                    if group_expr is not None:
                        stmt = stmt.where(group_expr)

            rows = conn.execute(stmt).mappings().all()
            return [{k: self.serialize_value(v) for k, v in row.items()} for row in rows]
    @staticmethod
    def serialize_value(value):
        if isinstance(value, (datetime, date)):
            return value.isoformat()
        elif isinstance(value, Decimal):
            return float(value)
        elif isinstance(value, uuid.UUID):
            return str(value)
        elif isinstance(value, bytes):
            return value.hex() 
        elif isinstance(value, bool):
            return bool(value) 
        elif value is None:
            return None
        elif isinstance(value, (list, dict)):
            return json.dumps(value)
        return value

