import base64
import hashlib
import json
import logging
from abc import ABC, abstractmethod
from datetime import datetime as dt, timedelta
from typing import List
from urllib.parse import quote

import pandas as pd
import pytz
import requests
from dateutil.relativedelta import relativedelta
from py_jftech import format_date, is_workday, component, autowired, get_config, filter_weekend, next_workday

from api import DatumType, DataSync, Datum
from basic.dao import robo_index_datas as rid, robo_eco_datas as red, robo_fund_navs as rfn, robo_exrate as re

logger = logging.getLogger(__name__)


class JDCDataSync(DataSync, ABC):

    @autowired
    def __init__(self, datum: Datum = None):
        self._datum = datum
        self._config = get_config(__name__)

    @property
    def start_date(self):
        return filter_weekend(self._config['start-date'])

    @abstractmethod
    def datum_start_date(self, datum_id):
        pass

    @abstractmethod
    def build_urls(self, datum, start_date, page=0) -> str:
        pass

    @property
    @abstractmethod
    def datum_type(self) -> DatumType:
        pass

    @abstractmethod
    def store_date(self, datumid, datas: List[dict]):
        pass

    def do_sync(self, max_date=dt.today()):
        logger.info(f'start sync datas for type[{self.datum_type}]')
        for datum in self._datum.get_datums(type=self.datum_type):
            logger.debug(f'start sync ticker[{datum["bloombergTicker"]}]')
            page = 0
            start_date = self.datum_start_date(datum['id'])
            while True:
                url = self.build_urls(datum=datum, page=page, start_date=start_date)
                if url is None:
                    break
                response = requests.get(url).json()
                if not response['success']:
                    raise Exception(f'''request indictor failed: {response['status']}''')
                try:
                    self.store_date(datum['id'], response['body']['content'])
                except Exception as e:
                    logger.exception(f'url[{url}] store data failed')
                    raise e
                if response['body']['last']:
                    break
                else:
                    page += 1


class TWDataSync(DataSync, ABC):
    @autowired
    def __init__(self, datum: Datum = None):
        self._datum = datum
        self._config = get_config(__name__)

    def do_sync(self, max_date=dt.today()):
        logger.info(f'start sync datas for type[{self.datum_type}]')
        response = self.get_all_data(self.start_date)
        for datum in self._datum.get_datums(type=self.datum_type):
            logger.debug(f'start sync ticker[{datum["ftTicker"]}]')
            try:
                self.store_date(datum['id'], datum["ftTicker"], response)
            except Exception as e:
                logger.exception(f'''{datum['id']} store data failed''')
                raise e

    @property
    def start_date(self):
        return filter_weekend(self._config['start-date'])

    @abstractmethod
    def last_datum(self, datum_id):
        pass

    @abstractmethod
    def get_all_data(self, start_date=dt.today()):
        pass

    @property
    @abstractmethod
    def datum_type(self) -> DatumType:
        pass

    @abstractmethod
    def store_date(self, datumid, ft_ticker, datas: List[dict]):
        pass


@component(bean_name='navs-sync')
class TWFundNavSync(TWDataSync):

    def get_all_data(self, start_date=dt.today()):
        return []

    def last_datum(self, datum_id):
        last = rfn.get_last_one(fund_id=datum_id)
        return last

    @property
    def datum_type(self) -> DatumType:
        return DatumType.FUND

    def store_date(self, datumid, ft_ticker, datas: List[dict]):
        last = self.last_datum(datum_id=datumid)
        start_date = next_workday(last['nav_date']) if last else self.start_date
        save_navs = []
        datas = requests.get(
            f"""https://cmsapi.franklin-dm.dev/funds/{ft_ticker}/nav?startDate={start_date.strftime('%Y-%m-%d')}""").json()
        if len(datas) > 0:
            for data in datas:
                div = data.get('fundDiv') or 0
                nav = {
                    'fund_id': datumid,
                    'nav_date': dt.fromisoformat(data['nav_Date']),
                    'av': data['nav_P'],
                    'div': div,
                    'split': 1,
                    'accrue_split': 1,
                    'av_p': data['nav_P'],
                    'div_p': div,
                    'nav_cal': round(data['nav_P'] * data['nav_Unit'], 4)
                }
                save_navs.append(nav)
        if save_navs:
            rfn.batch_insert(save_navs)


@component(bean_name='index-sync')
class IndexSync(JDCDataSync):

    @property
    def start_date(self):
        return super(IndexSync, self).start_date - relativedelta(years=4)

    @property
    def datum_type(self) -> DatumType:
        return DatumType.INDEX

    def datum_start_date(self, datum_id):
        last = rid.get_last_one(index_id=datum_id)
        return next_workday(last['date']) if last else self.start_date

    def build_urls(self, datum, start_date, page=0) -> str:
        sourceCode = quote(datum["bloombergTicker"]) if quote(datum["bloombergTicker"]) else quote(datum["thsTicker"])
        sourceType = 'BLOOMBERG' if quote(datum["bloombergTicker"]) else 'THS'
        return f'http://jdcprod.thiztech.com/api/datas/index-value?page={page}&size=200&sourceCode={sourceCode}&sourceType={sourceType}&startDate={format_date(start_date)}'

    def store_date(self, datumid, datas: List[dict]):
        # add frdpe，frdpes,erp，pc
        save_datas = [{
            'index_id': datumid,
            'date': dt.fromtimestamp(x['date'] / 1000, tz=pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d'),
            'close': x['close'],
            'open': x['open'] if 'open' in x else None,
            'high': x['high'] if 'high' in x else None,
            'low': x['low'] if 'low' in x else None,
            'pe': x['peRatio'] if 'peRatio' in x else None,
            'pb': x['pbRatio'] if 'pbRatio' in x else None,
            'volume': x['volume'] if 'volume' in x else None,
            'frdpe': x['forwardPe'] if 'forwardPe' in x else None,
            'frdpes': x['forwardEps'] if 'forwardEps' in x else None,
            'erp': x['erp'] if 'erp' in x else None,
            'pc': x['pcRatio'] if 'pcRatio' in x else None,
        } for x in datas if
            is_workday(dt.fromtimestamp(x['date'] / 1000, tz=pytz.timezone('Asia/Shanghai'))) and 'close' in x]
        if save_datas:
            rid.batch_insert(save_datas)

def ths_token():
    # Token accessToken 及权限校验机制
    getAccessTokenUrl = 'https://quantapi.51ifind.com/api/v1/get_access_token'
    refreshtoken = 'eyJzaWduX3RpbWUiOiIyMDI1LTA1LTE5IDE5OjE5OjM5In0=.eyJ1aWQiOiI3NzE0ODI3NzAiLCJ1c2VyIjp7ImFjY291bnQiOiJzaGpmc3kwMDEiLCJhdXRoVXNlckluZm8iOnsiU0ZUU0UiOnRydWUsIlNVU0FJbmRleENvZGUiOnRydWUsIlNEQ0UiOnRydWUsIlNMTUUiOnRydWUsIlNDSUNDIjp0cnVlLCJhcGlGb3JtYWwiOiIxIn0sImNvZGVDU0kiOltdLCJjb2RlWnpBdXRoIjpbXSwiaGFzQUlQcmVkaWN0IjpmYWxzZSwiaGFzQUlUYWxrIjpmYWxzZSwiaGFzQ0lDQyI6dHJ1ZSwiaGFzQ1NJIjpmYWxzZSwiaGFzRXZlbnREcml2ZSI6ZmFsc2UsImhhc0ZUU0UiOnRydWUsImhhc0Zhc3QiOmZhbHNlLCJoYXNGdW5kVmFsdWF0aW9uIjpmYWxzZSwiaGFzSEsiOnRydWUsImhhc0xNRSI6dHJ1ZSwiaGFzTGV2ZWwyIjpmYWxzZSwiaGFzUmVhbENNRSI6ZmFsc2UsImhhc1RyYW5zZmVyIjpmYWxzZSwiaGFzVVMiOmZhbHNlLCJoYXNVU0FJbmRleCI6dHJ1ZSwiaGFzVVNERUJUIjpmYWxzZSwibWFya2V0QXV0aCI6eyJEQ0UiOmZhbHNlfSwibWFya2V0Q29kZSI6IjE2OzMyOzE0NDsxNzY7MTEyOzg4OzQ4OzEyODsxNjgtMTsxODQ7MjAwOzIxNjsxMDQ7MTIwOzEzNjsyMzI7NTY7OTY7MTYwOzY0OyIsIm1heE9uTGluZSI6MSwibm9EaXNrIjpmYWxzZSwicHJvZHVjdFR5cGUiOiJTVVBFUkNPTU1BTkRQUk9EVUNUIiwicmVmcmVzaFRva2VuRXhwaXJlZFRpbWUiOiIyMDI4LTAzLTMxIDE5OjExOjQzIiwic2Vzc3Npb24iOiI1MTgzMzQxNDM2YWUxMTA3N2M3OGQwODYwZDhkODY5MCIsInNpZEluZm8iOnt9LCJ0cmFuc0F1dGgiOmZhbHNlLCJ1aWQiOiI3NzE0ODI3NzAiLCJ1c2VyVHlwZSI6Ik9GRklDSUFMIiwid2lmaW5kTGltaXRNYXAiOnt9fX0=.51B86B4E892B5B90BB134429C30D0E1F7FBA86258898D1030017C0CA1184FBA1'
    getAccessTokenHeader = {"Content-Type": "application/json", "refresh_token": refreshtoken}
    getAccessTokenResponse = requests.post(url=getAccessTokenUrl, headers=getAccessTokenHeader)
    accessToken = json.loads(getAccessTokenResponse.content)['data']['access_token']
    return accessToken

@component(bean_name='eco-sync')
class EcoSync(DataSync):

    @autowired
    def __init__(self, datum: Datum = None):
        self._datum = datum
        self._config = get_config(__name__)

    @property
    def start_date(self):
        return filter_weekend(self._config['start-date'])

    @property
    def datum_type(self) -> DatumType:
        return DatumType.ECO

    def datum_start_date(self, datum_id):
        last = red.get_last_one(eco_id=datum_id)
        return next_workday(last['date']) if last else self.start_date

    def do_sync(self, max_date=dt.today()):
        logger.info(f'start sync datas for type[{self.datum_type}]')
        for datum in self._datum.get_datums(type=self.datum_type):
            logger.debug(f'start sync ticker[{datum["bloombergTicker"]}]')
            start_date = self.datum_start_date(datum['id'])
            response = self.ths_eco(code=datum['thsTicker'],start_date=start_date)
            try:
                self.store_date(datum['id'], response['tables'][0])
            except Exception as e:
                logger.exception(f'''datumid[{datum['id']}] store data failed''')
                raise e

    def store_date(self, datumid, datas):
        keys = ['time', 'value','rtime']
        values = [datas[k] for k in keys]
        datas = [dict(zip(keys, row)) for row in zip(*values)]
        save_datas = [{
            'eco_id': datumid,
            'date': x['time'],
            'indicator': x['value'],
            'release_date': x['rtime'],
        } for x in datas]
        if save_datas:
            red.batch_insert(save_datas)

    def ths_eco(self, code,start_date):
        """
        经济数据库(EDB)-中国经济-社会消费品零售总额:当月同比-iFinD数据接口
        requestMethod:POST
        requestURL:https://quantapi.51ifind.com/api/v1/edb_service
        requestHeaders:{"Content-Type":"application/json","access_token":"81573051b7978789229c4ae00d31c00c678248d5.signs_NzcxNDgyNzcw","ifindlang":"cn"}
        formData:{"indicators":"M001625520","startdate":"2025-01-23","enddate":"2026-01-23"}
        """
        url = "https://quantapi.51ifind.com/api/v1/edb_service"
        headers = {
            "Content-Type": "application/json",
            "access_token": ths_token(),
            "ifindlang": "cn"
        }
        formData = {
            "indicators": code,
            "startdate": start_date.strftime('%Y-%m-%d'),
            "enddate": dt.today().strftime('%Y-%m-%d')
        }
        response = requests.post(url, json=formData, headers=headers)
        return response.json()


@component(bean_name='navs-sync')
class FundNavSync(JDCDataSync):

    def __init__(self):
        super(FundNavSync, self).__init__()
        self._jdc_querys = self.find_jdc_querys()

    @property
    def datum_type(self) -> DatumType:
        return DatumType.FUND

    def datum_start_date(self, datum_id):
        last = rfn.get_last_one(fund_id=datum_id)
        return next_workday(last['nav_date']) if last else self.start_date

    def build_urls(self, datum, start_date, page=0) -> str:
        if datum['id'] not in self._jdc_querys:
            return None
        querys = self._jdc_querys[datum['id']]
        query_str = '&'.join([f'{x[0]}={quote(str(x[1]).encode())}' for x in querys.items()])
        return f'http://jdcprod.thiztech.com/api/datas/asset-value?page={page}&size=200&startDate={format_date(start_date)}&{query_str}'

    def find_jdc_querys(self):
        funds = self._datum.get_datums(type=DatumType.FUND, exclude=False)
        urls = {x['id']: {
            'sourceCode': x['bloombergTicker'],
            'sourceType': 'BLOOMBERG'
        } for x in funds if 'ftTicker' not in x and 'bloombergTicker' in x}

        ft_tickers = {x['ftTicker']: x for x in funds if 'ftTicker' in x}
        response = requests.get('http://jdcprod.thiztech.com/api/subject?busiField=TW&sourceType=TW&subjectType=FUND')
        response = response.json()
        if not response['success']:
            raise CollectError(f'''find fund subject failed: {response['status']}''')
        return {**urls, **{
            ft_tickers[x['fundId']]['id']: {
                'subjectKeys': x['key'],
                'sourceType': 'TW'
            } for x in response['body']['content'] if x['fundId'] in ft_tickers
        }}

    def store_date(self, datumid, datas: List[dict]):
        save_navs = [{
            'fund_id': datumid,
            'nav_date': dt.fromtimestamp(x['date'] / 1000, tz=pytz.timezone('Asia/Shanghai')),
            'av': x['originValue'],
            'div': x['dividend'] if 'dividend' in x else 0,
            'split': x['split'] if 'split' in x else 1,
            'accrue_split': x['totalSplit'] if 'totalSplit' in x else 1,
            'av_p': x['postValue'],
            'div_p': x['postDividend'] if 'postDividend' in x else 0,
            'nav_cal': x['calibrateValue']
        } for x in datas if is_workday(dt.fromtimestamp(x['date'] / 1000, tz=pytz.timezone('Asia/Shanghai')))]
        if save_navs:
            rfn.batch_insert(save_navs)


@component(bean_name='exrate-sync')
class ExrateSync(DataSync):

    def __init__(self):
        self._config = get_config(__name__)

    @property
    def start_date(self):
        return filter_weekend(self._config['start-date'])

    @property
    def exrate_tickers(self):
        navs_config = get_config('basic.navs')
        return [x['ticker'] for x in navs_config['exrate']] if 'exrate' in navs_config else []

    def do_sync(self, max_date=dt.today()):
        logger.info(f'start sync datas for type[EXRATE]')
        for ticker in self.exrate_tickers:
            last_one = re.get_last_one(ticker=ticker)
            start_date = next_workday(last_one['date']) if last_one else self.start_date
            response = self.exrate_ths(code=ticker, start_date=start_date)
            response = response['tables']
            if len(response) == 0  or response[0].get('time') is None or len(response[0]['time']) == 0:
                continue
            response = response['tables'][0]
            save_dates = [{
                'ticker': ticker,
                'date': date,
                'close': close,
            } for date,close in zip(response['time'], response['table']['close'])]
            if save_dates:
                re.batch_insert(save_dates)

    def exrate_ths(self, code, start_date):
        """
        历史行情-外汇-收盘价-iFinD数据接口
        requestMethod:POST
        requestURL:https://quantapi.51ifind.com/api/v1/cmd_history_quotation
        requestHeaders:{"Content-Type":"application/json","access_token":"81573051b7978789229c4ae00d31c00c678248d5.signs_NzcxNDgyNzcw","ifindlang":"cn"}
        formData:{"codes":"EURUSD.FX","indicators":"close","startdate":"2025-01-20","enddate":"2026-01-23"}
        """
        url = "https://quantapi.51ifind.com/api/v1/cmd_history_quotation"
        headers = {
            "Content-Type": "application/json",
            "access_token": ths_token(),
            "ifindlang": "cn"
        }
        formData = {
            "codes": code,
            "indicators": "close",
            "startdate": start_date.strftime('%Y-%m-%d'),
            "enddate": dt.today().strftime('%Y-%m-%d')
        }
        response = requests.post(url, json=formData, headers=headers)
        return response.json()
