import multiprocessing as mp
import time
from pathlib import Path
from threading import Lock, Thread
from typing import Callable

from apscheduler.schedulers.background import BackgroundScheduler

from ldnode.runner.config import Settings
from ldnode.runner.schemas import Worker

from minio import Minio

from .datasources import (
    CBQuotationsDataSource,
    COSDataSource,
    ETFQuotationsDataSource,
    S3QuotationDataSource,
    S3WithCOSFallbackDataSource,
    StkQuotationsDataSource,
)
from .files import (
    CB_DATA,
    CB_HIST_DATA,
    CB_PRE_QUOTATION,
    CB_QUOTATION,
    ETF_DATA,
    ETF_PRE_QUOTATION,
    ETF_QUOTATION,
    INDEX_DATA,
    STK_DATA,
    STK_HIST_DATA,
    STK_PRE_QUOTATION,
    STK_QUOTATION,
    TRADE_DAYS,
)
from .persistence import LightCOSClient
from .schemas.models import LoadRule, LoadTrigger, SyncMetadata
from .stop import is_trading_time


class ContinuousLoader:
    """连续加载器"""

    def __init__(self):
        self.threads: dict[str, Thread] = {}

    def _cycle(self, func: Callable[[], bool]) -> None:
        continue_flag = True
        while continue_flag:
            continue_flag = func()

    def add_job(self, key: str, func: Callable[[], bool]) -> None:
        if key not in self.threads:
            self.threads[key] = Thread(target=self._cycle, args=(func,), daemon=True)

    def start(self) -> None:
        for thread in self.threads.values():
            thread.start()

    def shutdown(self) -> None:
        for thread in self.threads.values():
            thread.join()


class LoaderWorker(Worker):
    """加载工作进程"""

    def __init__(self, worker_name: str):
        super().__init__(worker_name)
        self.ap_scheduler = BackgroundScheduler()
        self.continuous_loader = ContinuousLoader()
        self.locks: dict[str, Lock] = {}
        self.output: mp.Queue | None = None

    def _get_cos_datasource(
        self, settings: Settings, metadata: SyncMetadata
    ) -> COSDataSource:
        cos_client = LightCOSClient(
            region=settings.DATA_SYNC_COS_REGION,
            secret_id=settings.DATA_SYNC_COS_SECRET_ID,
            secret_key=settings.DATA_SYNC_COS_SECRET_KEY,
            bucket=settings.DATA_SYNC_COS_BUCKET,
        )
        return COSDataSource(
            cos_client, metadata, Path(settings.DATA_SYNC_LOCAL_DATA_DIR)
        )

    def _wrap_load(self, rule: LoadRule) -> Callable[[], bool]:
        """包装加载函数"""

        def load() -> bool:
            if not self.running:
                return False
            if rule.is_stop and rule.is_stop():
                return True
            if rule.file.key not in self.locks:
                self.locks[rule.file.key] = Lock()
            with self.locks[rule.file.key]:
                results = rule.datasource.load(rule.file.key)
                if self.output:
                    for result in results:
                        self.output.put(result)
            return True

        return load

    def _get_s3_storage_options(self, settings: Settings) -> dict:
        """构造 S3 storage_options（包含 MinIO 兼容所需的 allow_http / region）"""
        opts: dict = {
            "allow_http": "true",
            "aws_region": "us-east-1",
        }
        if settings.QUOTATION_S3_KEY_ID:
            opts["aws_access_key_id"] = settings.QUOTATION_S3_KEY_ID
            opts["aws_secret_access_key"] = settings.QUOTATION_S3_SECRET_KEY
        if settings.QUOTATION_S3_ENDPOINT_URL:
            opts["endpoint_url"] = settings.QUOTATION_S3_ENDPOINT_URL
        return opts

    @staticmethod
    def _build_data_s3_client(settings: Settings) -> Minio | None:
        """构建基础数据的 S3 客户端（内网优先读取），未配置时返回 None"""
        if not settings.DATA_S3_ENDPOINT_URL:
            return None
        endpoint = settings.DATA_S3_ENDPOINT_URL
        secure = not endpoint.startswith("http://")
        host = endpoint.replace("http://", "").replace("https://", "")
        return Minio(
            host,
            access_key=settings.DATA_S3_KEY_ID,
            secret_key=settings.DATA_S3_SECRET_KEY,
            secure=secure,
            region="us-east-1",
        )

    def init_load_rules(self, settings: Settings) -> list[LoadRule]:
        """初始化加载规则。
        基础数据：配置了 DATA_S3 时优先从内网 S3 读取，fallback 到 COS。
        实时行情：配置了 S3 URI 时从 S3 预拉取，否则本地计算。
        output_lazyframe() 始终读本地文件，无感知切换。
        """
        local_data_dir = Path(settings.DATA_SYNC_LOCAL_DATA_DIR)
        metadata = SyncMetadata(local_data_dir)
        cos = self._get_cos_datasource(settings, metadata)
        s3_opts = self._get_s3_storage_options(settings)

        # 基础数据源：配置了 DATA_S3 则 S3 优先 + COS fallback，否则纯 COS
        data_s3_client = self._build_data_s3_client(settings)
        if data_s3_client:
            data_src = S3WithCOSFallbackDataSource(
                minio_client=data_s3_client,
                bucket=settings.DATA_S3_BUCKET,
                cos_datasource=cos,
                metadata=metadata,
                local_data_dir=local_data_dir,
            )
        else:
            data_src = cos

        rules = [
            LoadRule(TRADE_DAYS, LoadTrigger.INTERVAL, data_src, 10),
            LoadRule(INDEX_DATA, LoadTrigger.INTERVAL, data_src, 10),
            LoadRule(CB_DATA, LoadTrigger.INTERVAL, data_src, 10),
            LoadRule(CB_PRE_QUOTATION, LoadTrigger.INTERVAL, data_src, 10),
            LoadRule(CB_HIST_DATA, LoadTrigger.INTERVAL, data_src, 10),
        ]
        # 转债实时行情：S3 后台预拉取 OR 本地计算
        if settings.CB_QUOTATION_S3_URI:
            rules.append(LoadRule(
                CB_QUOTATION, LoadTrigger.INTERVAL,
                S3QuotationDataSource(settings.CB_QUOTATION_S3_URI, s3_opts, local_data_dir),
                3, is_stop=lambda: not is_trading_time(),
            ))
        else:
            rules.append(LoadRule(
                CB_QUOTATION, LoadTrigger.INTERVAL, CBQuotationsDataSource(), 3,
                is_stop=lambda: not is_trading_time(),
            ))

        rules += [
            LoadRule(STK_DATA, LoadTrigger.INTERVAL, data_src, 10),
            LoadRule(STK_PRE_QUOTATION, LoadTrigger.INTERVAL, data_src, 10),
            LoadRule(STK_HIST_DATA, LoadTrigger.INTERVAL, data_src, 10),
        ]
        # 股票实时行情：S3 后台预拉取 OR 本地计算
        if settings.STK_QUOTATION_S3_URI:
            rules.append(LoadRule(
                STK_QUOTATION, LoadTrigger.INTERVAL,
                S3QuotationDataSource(settings.STK_QUOTATION_S3_URI, s3_opts, local_data_dir),
                3, is_stop=lambda: not is_trading_time(),
            ))
        else:
            rules.append(LoadRule(
                STK_QUOTATION, LoadTrigger.INTERVAL, StkQuotationsDataSource(), 3,
                is_stop=lambda: not is_trading_time(),
            ))

        rules += [
            LoadRule(ETF_DATA, LoadTrigger.INTERVAL, data_src, 10),
            LoadRule(ETF_PRE_QUOTATION, LoadTrigger.INTERVAL, data_src, 10),
        ]
        # ETF 实时行情：S3 后台预拉取 OR 本地计算
        if settings.ETF_QUOTATION_S3_URI:
            rules.append(LoadRule(
                ETF_QUOTATION, LoadTrigger.INTERVAL,
                S3QuotationDataSource(settings.ETF_QUOTATION_S3_URI, s3_opts, local_data_dir),
                3, is_stop=lambda: not is_trading_time(),
            ))
        else:
            rules.append(LoadRule(
                ETF_QUOTATION, LoadTrigger.INTERVAL, ETFQuotationsDataSource(), 3,
                is_stop=lambda: not is_trading_time(),
            ))

        return rules

    def setup_load_rules(self, settings: Settings) -> None:
        """设置加载规则"""
        for rule in self.init_load_rules(settings):
            match rule.tigger:
                case LoadTrigger.INTERVAL:
                    self.ap_scheduler.add_job(
                        self._wrap_load(rule),
                        "interval",
                        id=rule.file.key,
                        name=rule.file.key,
                        seconds=rule.interval,
                    )
                case LoadTrigger.CONTINUOUS:
                    self.continuous_loader.add_job(rule.file.key, self._wrap_load(rule))

    def run(self, settings: Settings, input: mp.Queue, output: mp.Queue) -> None:
        """运行工作进程"""
        self.output = output
        self.setup_load_rules(settings)
        self.ap_scheduler.start()
        self.continuous_loader.start()
        while self.running:
            time.sleep(1)
        self.ap_scheduler.shutdown()
        self.continuous_loader.shutdown()
