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 .datasources import (
    CBQuotationsDataSource,
    COSDataSource,
    ETFQuotationsDataSource,
    S3QuotationDataSource,
    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

    def init_load_rules(self, settings: Settings) -> list[LoadRule]:
        """初始化加载规则。
        配置了 S3 URI 的行情品种：注册 S3QuotationDataSource（后台每 3s 拉取写本地）。
        未配置 S3 URI：原有本地行情计算逻辑不变。
        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)

        rules = [
            LoadRule(TRADE_DAYS, LoadTrigger.INTERVAL, cos, 10),
            LoadRule(INDEX_DATA, LoadTrigger.INTERVAL, cos, 10),
            LoadRule(CB_DATA, LoadTrigger.INTERVAL, cos, 10),
            LoadRule(CB_PRE_QUOTATION, LoadTrigger.INTERVAL, cos, 10),
            LoadRule(CB_HIST_DATA, LoadTrigger.INTERVAL, cos, 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, cos, 10),
            LoadRule(STK_PRE_QUOTATION, LoadTrigger.INTERVAL, cos, 10),
            LoadRule(STK_HIST_DATA, LoadTrigger.INTERVAL, cos, 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, cos, 10),
            LoadRule(ETF_PRE_QUOTATION, LoadTrigger.INTERVAL, cos, 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()
