import json
import logging
from datetime import datetime as dt
from typing import List

from py_jftech import component, autowired, format_date
from pymysql import IntegrityError, constants

from api import PortfoliosBuilder, PortfoliosRisk, AssetPool, Navs, PortfoliosType, Datum, SolveType, SolverFactory, RoboReportor, DatumType
from portfolios.dao import robo_mpt_portfolios as rmp

logger = logging.getLogger(__name__)


@component(bean_name='mpt')
class MptPortfoliosBuilder(PortfoliosBuilder):

    @autowired
    def __init__(self, assets: AssetPool = None, navs: Navs = None, datum: Datum = None, factory: SolverFactory = None):
        self._assets = assets
        self._navs = navs
        self._datum = datum
        self._factory = factory

    def get_portfolios(self, day, risk: PortfoliosRisk, type: PortfoliosType = PortfoliosType.NORMAL):
        try:
            portfolio = rmp.get_one(day, type, risk)
            if not portfolio:
                result, detail = self.build_portfolio(day, type)
                for build_risk, datas in result.items():
                    try:
                        rmp.insert({
                            **datas,
                            'risk': build_risk,
                            'type': type,
                            'date': day
                        })
                    except IntegrityError as e:
                        code, msg = e.args
                        if code != constants.ER.DUP_ENTRY:
                            raise e
                portfolio = rmp.get_one(day, type, risk)
            if SolveType(portfolio['solve']) is not SolveType.INFEASIBLE:
                result = json.loads(portfolio['portfolio'])
                return {int(x[0]): x[1] for x in result.items()}
            return None
        except Exception as e:
            logger.exception(f"build protfolio of type[{type.name}] and risk[{risk.name}] with date[{format_date(day)}] failure.", e)
            raise e

    def build_portfolio(self, day, type: PortfoliosType):
        result = {}
        detail = {}
        for risk in PortfoliosRisk:
            logger.info(
                f"start to build protfolio of type[{type.name}] and risk[{risk.name}] with date[{format_date(day)}]")
            solver = self._factory.create_solver(risk, type)
            solver.reset_navs(day)
            logger.debug({
                'Khist': len(solver.rtn_history),
                'beta': solver.get_config('mpt.cvar-beta'),
                'Kbeta': solver.k_beta,
            })
            max_rtn, max_var, minCVaR_whenMaxR = solver.solve_max_rtn()
            min_rtn, min_var, maxCVaR_whenMinV = solver.solve_min_rtn()
            portfolio, cvar = solver.solve_mpt(min_rtn, max_rtn)
            result[risk] = {
                'solve': SolveType.MPT,
                'portfolio': json.dumps(portfolio),
                'cvar': cvar
            } if portfolio else {
                'solve': SolveType.INFEASIBLE
            }
            detail[risk] = {
                'max_rtn': max_rtn,
                'max_var': max_var,
                'minCVaR_whenMaxR': minCVaR_whenMaxR,
                'min_rtn': min_rtn,
                'min_var': min_var,
                'maxCVaR_whenMinV': maxCVaR_whenMinV,
            }
        return result, detail

    def clear(self, day=None, risk: PortfoliosRisk = None):
        rmp.delete(min_date=day, risk=risk)


@component(bean_name='poem')
class PoemPortfoliosBuilder(MptPortfoliosBuilder):

    def build_portfolio(self, day, type: PortfoliosType):
        result, detail = super(PoemPortfoliosBuilder, self).build_portfolio(day, type)
        for risk in PortfoliosRisk:
            if result[risk]['solve'] is SolveType.INFEASIBLE:
                continue
            solver = self._factory.create_solver(risk, type)
            solver.reset_navs(day)
            min_rtn = detail[risk]['min_rtn']
            max_rtn = detail[risk]['max_rtn']
            mpt_cvar = result[risk]['cvar']
            maxCVaR_whenMinV = detail[risk]['maxCVaR_whenMinV']
            portfolio, cvar = solver.solve_poem(min_rtn, max_rtn, mpt_cvar, maxCVaR_whenMinV)
            if portfolio:
                result[risk] = {
                    'solve': SolveType.POEM,
                    'portfolio': json.dumps(portfolio),
                    'cvar': cvar
                }
                detail[risk]['mpt_cvar'] = mpt_cvar
        return result, detail


@component(bean_name='mpt-report')
class MptReportor(RoboReportor):

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

    @property
    def report_name(self) -> str:
        return 'ζœ€δΌ˜ζŠ•η»„'

    def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]:
        results = []
        datums = {x['id']: x for x in self._datum.get_datums(type=DatumType.FUND)}
        for portfolio in rmp.get_list(max_date=max_date, min_date=min_date):
            solve_type = SolveType(portfolio['solve'])
            datas = {
                'date': portfolio['date'],
                'risk': PortfoliosRisk(portfolio['risk']).name,
                'type': PortfoliosType(portfolio['type']).name,
                'solve': solve_type.name,
                'cvar': portfolio['cvar']
            }
            if solve_type is not SolveType.INFEASIBLE:
                for asset_id, weight in json.loads(portfolio['portfolio']).items():
                    datum = datums[int(asset_id)]
                    results.append({
                        **datas,
                        'ft_ticker': datum['ftTicker'],
                        'lipper_id': datum['lipperKey'],
                        'bloomberg_ticker': datum['bloombergTicker'],
                        'name': datum['chineseName'],
                        'weight': weight,
                    })
            else:
                results.append(datas)
        return results