import json
import logging
from datetime import date
from functools import cached_property
from pathlib import Path
from typing import TYPE_CHECKING, Any, List, Optional, Dict

import pandas as pd
from bearish.database.crud import BearishDb  # type: ignore
from bearish.database.schemas import EarningsDateORM, EquityORM, PriceORM  # type: ignore
from bearish.models.base import Ticker  # type: ignore
from bearish.models.price.price import Price  # type: ignore
from bearish.models.price.prices import Prices  # type: ignore
from bearish.types import Sources  # type: ignore
from pydantic import ConfigDict
from sqlalchemy import Engine, create_engine, insert, delete, update, inspect
from sqlalchemy import text
from sqlmodel import Session, select

from bullish.analysis.analysis import Analysis, SubjectAnalysis
from bullish.analysis.constants import Industry, IndustryGroup, Sector, Country
from bullish.analysis.filter import FilteredResults
from bullish.analysis.indicators import SignalSeries
from bullish.analysis.industry_views import Type, IndustryView

from bullish.database.schemas import (
    AnalysisORM,
    JobTrackerORM,
    FilteredResultsORM,
    IndustryViewORM,
    SignalSeriesORM,
    BacktestResultORM,
    OpenAINewsORM,
)
from bullish.database.scripts.upgrade import upgrade
from bullish.exceptions import DatabaseFileNotFoundError
from bullish.interface.interface import BullishDbBase
from bullish.jobs.models import JobTracker, JobTrackerStatus
from tickermood.database.scripts.upgrade import upgrade as tickermood_upgrade  # type: ignore

if TYPE_CHECKING:
    from bullish.analysis.backtest import BacktestResult, BacktestResultQuery
    from bullish.analysis.openai import OpenAINews

logger = logging.getLogger(__name__)

BATCH_SIZE = 5000


class BullishDb(BearishDb, BullishDbBase):  # type: ignore
    model_config = ConfigDict(arbitrary_types_allowed=True)
    database_path: Path

    def valid(self) -> bool:
        """Check if the database is valid."""
        return self.database_path.exists() and self.database_path.is_file()

    @cached_property
    def _engine(self) -> Engine:
        if not self.valid():
            raise DatabaseFileNotFoundError("Database file not found.")
        database_url = f"sqlite:///{Path(self.database_path)}"
        try:
            upgrade(self.database_path)
        except Exception as e:
            logger.warning(
                f"Failed to upgrade the database at {self.database_path}. "
                f"Reason: {e}"
                "Skipping upgrade. "
            )
        engine = create_engine(database_url)
        inspector = inspect(engine)
        if "subject" not in inspector.get_table_names():
            logger.info(
                "Running tickermood upgrade to create the subject table in the database."
            )
            try:
                tickermood_upgrade(database_url=database_url, no_migration=True)
            except Exception as e:
                logger.error(f"failed to update database: {e}")
                print(f"failed to update database: {e}")
        return engine

    def model_post_init(self, __context: Any) -> None:
        self._engine  # noqa: B018

    def _write_analysis(self, analysis: Analysis) -> None:
        with Session(self._engine) as session:
            stmt = (
                insert(AnalysisORM)
                .prefix_with("OR REPLACE")
                .values(analysis.model_dump())
            )
            session.exec(stmt)  # type: ignore
            session.commit()

    def _write_many_analysis(self, many_analysis: List[Analysis]) -> None:
        with Session(self._engine) as session:
            stmt = (
                insert(AnalysisORM)
                .prefix_with("OR REPLACE")
                .values([a.model_dump() for a in many_analysis])
            )
            session.exec(stmt)  # type: ignore
            session.commit()

    def _read_analysis(self, ticker: Ticker) -> Optional[Analysis]:
        with Session(self._engine) as session:
            query = select(AnalysisORM).where(AnalysisORM.symbol == ticker.symbol)
            analysis = session.exec(query).first()
            if not analysis:
                return None
            return Analysis.model_validate(analysis)

    def read_symbols(self) -> List[str]:
        query = "SELECT DISTINCT symbol FROM analysis"
        data = pd.read_sql_query(query, self._engine)
        return data["symbol"].tolist()

    def _read_analysis_data(
        self, columns: List[str], symbols: Optional[List[str]] = None
    ) -> pd.DataFrame:
        columns_ = ",".join(columns)
        if symbols:
            symbols_str = ",".join([f"'{s}'" for s in symbols])
            query = f"""SELECT {columns_} FROM analysis WHERE symbol IN ({symbols_str})"""  # noqa: S608
        else:
            query = f"""SELECT {columns_} FROM analysis"""  # noqa: S608
        return pd.read_sql_query(query, self._engine)

    def _read_filter_query(self, query: str) -> pd.DataFrame:
        return pd.read_sql(
            query,
            con=self._engine,
        )

    def _read_job_trackers(self) -> pd.DataFrame:
        query = "SELECT * FROM jobtracker ORDER BY started_at DESC"
        return pd.read_sql_query(query, self._engine)

    def write_job_tracker(self, job_tracker: JobTracker) -> None:
        with Session(self._engine) as session:
            stmt = (
                insert(JobTrackerORM)
                .prefix_with("OR REPLACE")
                .values(job_tracker.model_dump())
            )
            session.exec(stmt)  # type: ignore
            session.commit()

    def read_job_tracker(self, task_id: str) -> Optional[JobTracker]:
        stmt = select(JobTrackerORM).where(JobTrackerORM.job_id == task_id)
        with Session(self._engine) as session:
            result = session.execute(stmt).scalar_one_or_none()
            if result:
                return JobTracker.model_validate(result.model_dump())
            return None

    def delete_job_trackers(self, job_ids: List[str]) -> None:
        with Session(self._engine) as session:
            stmt = delete(JobTrackerORM).where(JobTrackerORM.job_id.in_(job_ids))  # type: ignore
            result = session.execute(stmt)

            if result.rowcount > 0:  # type: ignore
                session.commit()
            else:
                logger.warning(f"Job tracker(s) with ID(s) {job_ids} not found.")

    def update_job_tracker_status(self, job_tracker_status: JobTrackerStatus) -> None:
        with Session(self._engine) as session:
            stmt = (
                update(JobTrackerORM)
                .where(JobTrackerORM.job_id == job_tracker_status.job_id)  # type: ignore
                .values(status=job_tracker_status.status)
            )
            result = session.execute(stmt)

            if result.rowcount > 0:  # type: ignore
                session.commit()
            else:
                logger.warning(
                    f"Job tracker with ID {job_tracker_status.job_id} not found."
                )

    def read_filtered_results(self, name: str) -> Optional[FilteredResults]:
        with Session(self._engine) as session:
            stmt = select(FilteredResultsORM).where(FilteredResultsORM.name == name)
            result = session.execute(stmt).scalar_one_or_none()

            if result:
                return FilteredResults.model_validate(
                    result.model_dump()
                )  # if you're using Pydantic or DTOs
            return None

    def read_list_filtered_results(self) -> List[str]:
        with Session(self._engine) as session:
            stmt = select(FilteredResultsORM.name)
            result = session.execute(stmt).scalars().all()
            return list(result)

    def write_filtered_results(self, filtered_results: FilteredResults) -> None:
        with Session(self._engine) as session:
            data = filtered_results.model_dump_json(
                exclude_unset=True, exclude_defaults=True
            )
            stmt = (
                insert(FilteredResultsORM)
                .prefix_with("OR REPLACE")
                .values(json.loads(data))
            )
            session.exec(stmt)  # type: ignore
            session.commit()

    def read_dates(self, symbol: str) -> List[date]:
        with Session(self._engine) as session:
            return [
                r.date()
                for r in session.exec(
                    select(EarningsDateORM.date).where(EarningsDateORM.symbol == symbol)
                )
            ]

    def read_industry_symbols(
        self, industries: List[Industry], country: Country, source: Sources = "Yfinance"
    ) -> List[str]:
        with Session(self._engine) as session:
            stmt = select(EquityORM.symbol).where(
                EquityORM.industry.in_(industries),
                EquityORM.source == source,
                EquityORM.country == country,
            )
            result = session.exec(stmt).all()
            return list(result)

    def read_industry_group_symbols(
        self,
        industry_groups: List[IndustryGroup],
        country: Country,
        source: Sources = "Yfinance",
    ) -> List[str]:
        with Session(self._engine) as session:
            stmt = select(EquityORM.symbol).where(
                EquityORM.industry_group.in_(industry_groups),
                EquityORM.source == source,
                EquityORM.country == country,
            )
            result = session.exec(stmt).all()
            return list(result)

    def read_sector_symbols(
        self, sectors: List[Sector], country: Country, source: Sources = "Yfinance"
    ) -> List[str]:
        with Session(self._engine) as session:
            stmt = select(EquityORM.symbol).where(
                EquityORM.sector.in_(sectors),
                EquityORM.source == source,
                EquityORM.country == country,
            )
            result = session.exec(stmt).all()
            return list(result)

    def write_returns(self, industry_returns: List[IndustryView]) -> None:
        with Session(self._engine) as session:
            stmt = (
                insert(IndustryViewORM)
                .prefix_with("OR REPLACE")
                .values([a.model_dump() for a in industry_returns])
            )
            session.exec(stmt)  # type: ignore
            session.commit()

    def read_returns(
        self, type: Type, industry: Industry, country: Country
    ) -> List[IndustryView]:
        with Session(self._engine) as session:
            stmt = select(IndustryViewORM)
            if industry:
                stmt = stmt.where(IndustryViewORM.industry == industry)
            if country:
                stmt = stmt.where(IndustryViewORM.country == country)
            result = session.exec(stmt).all()
            return [IndustryView.model_validate(r) for r in result]

    def write_signal_series(self, signal_series: List[SignalSeries]) -> None:
        with Session(self._engine) as session:
            stmt = (
                insert(SignalSeriesORM)
                .prefix_with("OR REPLACE")
                .values([a.model_dump() for a in signal_series])
            )
            session.exec(stmt)  # type: ignore
            session.commit()

    def read_signal_series(
        self, name: str, start_date: date, end_date: date
    ) -> List[str]:
        with Session(self._engine) as session:
            stmt = select(SignalSeriesORM.symbol).where(
                SignalSeriesORM.name == name,
                SignalSeriesORM.date >= start_date,  # type: ignore
                SignalSeriesORM.date <= end_date,  # type: ignore
            )
            return list(set(session.exec(stmt).all()))

    def read_symbol_series(
        self, symbol: str, start_date: date, end_date: Optional[date] = None
    ) -> pd.DataFrame:

        with Session(self._engine) as session:
            query_ = select(PriceORM)
            query_ = query_.where(PriceORM.symbol == symbol)
            if end_date:
                query_ = query_.where(
                    PriceORM.date >= start_date, PriceORM.date <= end_date
                )
            else:
                query_ = query_.where(PriceORM.date >= start_date)
            series = session.exec(query_).all()
            prices = [Price.model_validate(serie) for serie in series]
            return Prices(prices=prices).to_dataframe()  # type: ignore

    def write_many_backtest_results(
        self, backtest_results: List["BacktestResult"]
    ) -> None:
        with Session(self._engine) as session:
            stmt = (
                insert(BacktestResultORM)
                .prefix_with("OR REPLACE")
                .values([a.model_dump() for a in backtest_results])
            )
            session.exec(stmt)  # type: ignore
            session.commit()

    def read_many_backtest_results(
        self, query: Optional["BacktestResultQuery"] = None
    ) -> List["BacktestResult"]:
        from bullish.analysis.backtest import BacktestResult

        with Session(self._engine) as session:
            stmt = select(BacktestResultORM)
            results = session.exec(stmt).all()
            if results:
                return [BacktestResult.model_validate(r) for r in results]
            else:
                return []

    def read_next_earnings_date(self, symbol: str) -> Optional[date]:
        with Session(self._engine) as session:
            stmt = select(EarningsDateORM.date).where(
                EarningsDateORM.symbol == symbol, EarningsDateORM.date > date.today()
            )
            result = session.exec(stmt).first()
            if result:
                return result.date()  # type: ignore
            return None

    def read_subject(self, symbol: str) -> Optional[SubjectAnalysis]:
        sql = text(
            """
            SELECT *
            FROM   subject
            WHERE  symbol = :symbol
            ORDER  BY date DESC
            LIMIT  1
        """
        )
        sql_oai = text(
            """
            SELECT *
            FROM   openai
            WHERE  symbol = :symbol
            ORDER  BY news_date DESC
            LIMIT  1
        """
        )

        with Session(self._engine) as session:
            row = session.execute(sql, {"symbol": symbol}).mappings().one_or_none()
            row_oai = (
                session.execute(sql_oai, {"symbol": symbol}).mappings().one_or_none()
            )
            row_dict = {}
            if row:
                row_dict = dict(row)
                row_dict = row_dict | {"news_date": row_dict["date"]}
            if row_oai:
                row_dict_oai = dict(row_oai)
                row_dict = row_dict | {
                    "oai_news_date": row_dict_oai.get("news_date"),
                    "oai_recent_news": row_dict_oai.get("recent_news"),
                    "oai_recommendation": row_dict_oai.get("recommendation"),
                    "oai_explanation": row_dict_oai.get("explanation"),
                    "oai_high_price_target": row_dict_oai.get("high_price_target"),
                    "oai_low_price_target": row_dict_oai.get("low_price_target"),
                    "oai_moat": row_dict_oai.get("moat"),
                }

            return SubjectAnalysis.model_validate(row_dict)

    def write_many_openai_news(self, openai_news: List["OpenAINews"]) -> None:
        with Session(self._engine) as session:
            stmt = (
                insert(OpenAINewsORM)
                .prefix_with("OR REPLACE")
                .values([a.model_dump() for a in openai_news])
            )
            session.exec(stmt)  # type: ignore
            session.commit()

    def update_analysis(self, symbol: str, fields: Dict[str, Any]) -> None:
        with Session(self._engine) as session:
            stmt = (
                update(AnalysisORM).where(AnalysisORM.symbol == symbol).values(**fields)  # type: ignore
            )
            session.exec(stmt)  # type: ignore
            session.commit()
