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 import logger
from ldnode.runner.config import Settings
from ldnode.runner.config.settings import NodeRole
from ldnode.runner.schemas import Worker

from minio import Minio

from .datasources import (
    CBQuotationsDataSource,
    COSDataSource,
    ETFQuotationsDataSource,
    S3QuotationDataSource,
    S3DataSource,
    S3WithCOSFallbackDataSource,
    StkQuotationsDataSource,
    QuotationComputeAndUploadDataSource,
)
from .files import (
    CB_DATA,
    CB_HIST_DATA,
    CB_PRE_QUOTATION,
    CB_QUOTATION,
    ETF_DATA,
    ETF_PRE_QUOTATION,
    ETF_QUOTATION,
    FINANCE_DATA,
    INDEX_DATA,
    STK_DATA,
    STK_FINA_IND,
    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

    @staticmethod
    def _build_s3_client(settings: Settings) -> Minio | None:
        """构建统一 S3 客户端，未配置时返回 None"""
        endpoint = settings.get_s3_endpoint()
        if not endpoint:
            return None
        secure = not endpoint.startswith("http://")
        host = endpoint.replace("http://", "").replace("https://", "")
        return Minio(
            host,
            access_key=settings.get_s3_access_key(),
            secret_key=settings.get_s3_secret_key(),
            secure=secure,
            region="us-east-1",
        )

    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 init_load_rules(self, settings: Settings) -> list[LoadRule]:
        """初始化加载规则（按 NODE_ROLE 分发）"""
        local_data_dir = Path(settings.DATA_SYNC_LOCAL_DATA_DIR)
        metadata = SyncMetadata(local_data_dir)
        s3_client = self._build_s3_client(settings)
        bucket = settings.get_s3_bucket()
        role = settings.NODE_ROLE

        logger.loader.info(f"[Loader] 节点角色: {role.value}, S3: {settings.get_s3_endpoint() or '未配置'}")

        # 基础数据源
        if s3_client and role in (NodeRole.MASTER, NodeRole.SLAVE):
            data_src = S3DataSource(s3_client, bucket, metadata, local_data_dir)
        elif s3_client:
            # standalone: S3 优先 + COS 兜底（向后兼容）
            cos = self._get_cos_datasource(settings, metadata)
            data_src = S3WithCOSFallbackDataSource(
                minio_client=s3_client, bucket=bucket,
                cos_datasource=cos, metadata=metadata, local_data_dir=local_data_dir,
            )
        else:
            cos = self._get_cos_datasource(settings, metadata)
            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),
            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),
            LoadRule(ETF_DATA, LoadTrigger.INTERVAL, data_src, 10),
            LoadRule(ETF_PRE_QUOTATION, LoadTrigger.INTERVAL, data_src, 10),
            LoadRule(FINANCE_DATA, LoadTrigger.INTERVAL, data_src, 600),
            LoadRule(STK_FINA_IND, LoadTrigger.INTERVAL, data_src, 600),
        ]

        # 实时行情：按角色决定
        not_trading = lambda: not is_trading_time()

        if role == NodeRole.SLAVE:
            logger.loader.info("[Loader] Slave 模式：从 S3 读取实时行情")
            rules += [
                LoadRule(CB_QUOTATION, LoadTrigger.INTERVAL,
                         S3QuotationDataSource(s3_client, bucket, local_data_dir), 3, is_stop=not_trading),
                LoadRule(STK_QUOTATION, LoadTrigger.INTERVAL,
                         S3QuotationDataSource(s3_client, bucket, local_data_dir), 3, is_stop=not_trading),
                LoadRule(ETF_QUOTATION, LoadTrigger.INTERVAL,
                         S3QuotationDataSource(s3_client, bucket, local_data_dir), 3, is_stop=not_trading),
            ]
        else:
            # master / standalone：本地计算
            cb_src = CBQuotationsDataSource()
            stk_src = StkQuotationsDataSource()
            etf_src = ETFQuotationsDataSource()

            if role == NodeRole.MASTER and s3_client:
                logger.loader.info("[Loader] Master 模式：本地计算行情 + 上传 S3")
                cb_src = QuotationComputeAndUploadDataSource(cb_src, s3_client, bucket, local_data_dir)
                stk_src = QuotationComputeAndUploadDataSource(stk_src, s3_client, bucket, local_data_dir)
                etf_src = QuotationComputeAndUploadDataSource(etf_src, s3_client, bucket, local_data_dir)
            else:
                logger.loader.info("[Loader] Standalone 模式：本地计算行情")

            rules += [
                LoadRule(CB_QUOTATION, LoadTrigger.INTERVAL, cb_src, 3, is_stop=not_trading),
                LoadRule(STK_QUOTATION, LoadTrigger.INTERVAL, stk_src, 3, is_stop=not_trading),
                LoadRule(ETF_QUOTATION, LoadTrigger.INTERVAL, etf_src, 3, is_stop=not_trading),
            ]

        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()
