from app.helpers.sparkline import load_sparkline


class MonteCarloService:
    def __init__(self):
        pass

    def simulate(self, symbols=None, asset_type='crypto', num_simulations=10000, days=30):
        import json
        import numpy as np
        from app.extensions import db
        from app.models.asset import Asset

        try:
            if symbols and isinstance(symbols, str):
                symbols = [symbols]

            if symbols:
                assets = Asset.query.filter(
                    Asset.symbol.in_([s.upper() for s in symbols]),
                    Asset.asset_type == asset_type
                ).all()
            else:
                assets = Asset.query.filter_by(asset_type=asset_type).order_by(
                    Asset.market_cap.desc()
                ).limit(10).all()

            if not assets:
                return {'status': 'error', 'message': 'No assets found'}

            results = []
            for asset in assets:
                try:
                    prices = load_sparkline(asset)
                    if len(prices) < 10:
                        results.append({
                            'symbol': asset.symbol,
                            'status': 'insufficient_data',
                            'message': f'Only {len(prices)} data points available'
                        })
                        continue

                    prices = np.array(prices, dtype=float)
                    prices = prices[prices > 0]
                    if len(prices) < 10:
                        results.append({
                            'symbol': asset.symbol,
                            'status': 'insufficient_data',
                            'message': 'Not enough valid price data'
                        })
                        continue

                    log_returns = np.diff(np.log(prices))
                    returns_mean = np.mean(log_returns)
                    returns_std = np.std(log_returns)
                    current_price = float(prices[-1])

                    simulated_paths = self._generate_paths(
                        returns_mean, returns_std, current_price, days, num_simulations
                    )

                    final_prices = simulated_paths[:, -1]
                    simulated_returns = (final_prices - current_price) / current_price

                    var_95 = self._calculate_var(simulated_returns, confidence=0.95)
                    var_99 = self._calculate_var(simulated_returns, confidence=0.99)
                    cvar_95 = self._calculate_cvar(simulated_returns, confidence=0.95)

                    max_drawdowns = np.zeros(num_simulations)
                    for i in range(num_simulations):
                        path = simulated_paths[i]
                        running_max = np.maximum.accumulate(path)
                        drawdowns = (path - running_max) / running_max
                        max_drawdowns[i] = np.min(drawdowns)

                    probability_of_profit = float(np.mean(simulated_returns > 0) * 100)
                    probability_of_ruin = float(np.mean(max_drawdowns < -0.5) * 100)

                    percentiles = {
                        '10th': float(np.percentile(final_prices, 10)),
                        '25th': float(np.percentile(final_prices, 25)),
                        '50th': float(np.percentile(final_prices, 50)),
                        '75th': float(np.percentile(final_prices, 75)),
                        '90th': float(np.percentile(final_prices, 90)),
                    }

                    results.append({
                        'symbol': asset.symbol,
                        'name': asset.name,
                        'status': 'success',
                        'current_price': current_price,
                        'simulated_median': float(np.median(final_prices)),
                        'simulated_mean': float(np.mean(final_prices)),
                        'var_95': float(var_95),
                        'var_99': float(var_99),
                        'cvar_95': float(cvar_95),
                        'max_dd_median': float(np.median(max_drawdowns)),
                        'max_dd_95th': float(np.percentile(max_drawdowns, 5)),
                        'probability_of_profit': probability_of_profit,
                        'probability_of_ruin': probability_of_ruin,
                        'percentiles': percentiles,
                        'num_simulations': num_simulations,
                        'days': days,
                        'returns_mean_daily': float(returns_mean),
                        'returns_std_daily': float(returns_std),
                    })
                except Exception as e:
                    results.append({
                        'symbol': asset.symbol,
                        'status': 'error',
                        'message': str(e)
                    })

            return {'status': 'success', 'data': results}

        except Exception as e:
            return {'status': 'error', 'message': str(e)}

    def _generate_paths(self, returns_mean, returns_std, current_price, days, num_sims):
        import numpy as np

        try:
            random_returns = np.random.normal(returns_mean, returns_std, size=(num_sims, days))
            cumulative_log_returns = np.cumsum(random_returns, axis=1)
            price_paths = current_price * np.exp(cumulative_log_returns)
            full_paths = np.column_stack([
                np.full(num_sims, current_price),
                price_paths
            ])
            return full_paths
        except Exception:
            return np.full((num_sims, days + 1), current_price)

    def _calculate_var(self, simulated_returns, confidence=0.95):
        import numpy as np

        try:
            alpha = 1 - confidence
            var = float(np.percentile(simulated_returns, alpha * 100))
            return var
        except Exception:
            return 0.0

    def _calculate_cvar(self, simulated_returns, confidence=0.95):
        import numpy as np

        try:
            var = self._calculate_var(simulated_returns, confidence)
            tail_returns = simulated_returns[simulated_returns <= var]
            if len(tail_returns) == 0:
                return var
            cvar = float(np.mean(tail_returns))
            return cvar
        except Exception:
            return 0.0

    def portfolio_simulation(self, asset_type='crypto', days=30, num_simulations=5000):
        import json
        import numpy as np
        from app.extensions import db
        from app.models.asset import Asset

        try:
            assets = Asset.query.filter_by(asset_type=asset_type).order_by(
                Asset.market_cap.desc()
            ).limit(20).all()

            if not assets:
                return {'status': 'error', 'message': 'No assets found'}

            valid_coins = []
            all_returns = []

            for asset in assets:
                try:
                    prices = load_sparkline(asset)
                    if len(prices) < 10:
                        continue
                    prices = np.array(prices, dtype=float)
                    prices = prices[prices > 0]
                    if len(prices) < 10:
                        continue
                    log_returns = np.diff(np.log(prices))
                    all_returns.append(log_returns)
                    valid_coins.append(asset)
                except Exception:
                    continue

            if len(valid_coins) < 2:
                return {'status': 'error', 'message': 'Insufficient assets with valid data'}

            min_len = min(len(r) for r in all_returns)
            all_returns = [r[-min_len:] for r in all_returns]
            returns_matrix = np.array(all_returns)

            n_coins = len(valid_coins)
            weights = np.ones(n_coins) / n_coins

            portfolio_returns = returns_matrix.T @ weights
            port_mean = float(np.mean(portfolio_returns))
            port_std = float(np.std(portfolio_returns))

            simulated_paths = self._generate_paths(
                port_mean, port_std, 1.0, days, num_simulations
            )

            final_values = simulated_paths[:, -1]
            portfolio_sim_returns = final_values - 1.0

            var_95 = self._calculate_var(portfolio_sim_returns, confidence=0.95)
            var_99 = self._calculate_var(portfolio_sim_returns, confidence=0.99)
            cvar_95 = self._calculate_cvar(portfolio_sim_returns, confidence=0.95)

            max_drawdowns = np.zeros(num_simulations)
            for i in range(num_simulations):
                path = simulated_paths[i]
                running_max = np.maximum.accumulate(path)
                drawdowns = (path - running_max) / running_max
                max_drawdowns[i] = np.min(drawdowns)

            probability_of_profit = float(np.mean(portfolio_sim_returns > 0) * 100)
            probability_of_ruin = float(np.mean(max_drawdowns < -0.5) * 100)

            coin_weights = []
            for i, asset in enumerate(valid_coins):
                coin_weights.append({
                    'symbol': asset.symbol,
                    'name': asset.name,
                    'weight': float(weights[i]),
                })

            percentiles = {
                '10th': float(np.percentile(final_values, 10)),
                '25th': float(np.percentile(final_values, 25)),
                '50th': float(np.percentile(final_values, 50)),
                '75th': float(np.percentile(final_values, 75)),
                '90th': float(np.percentile(final_values, 90)),
            }

            result = {
                'strategy': 'equal_weight',
                'num_coins': n_coins,
                'assets': coin_weights,
                'portfolio_mean_daily_return': port_mean,
                'portfolio_std_daily_return': port_std,
                'simulated_median_value': float(np.median(final_values)),
                'simulated_mean_value': float(np.mean(final_values)),
                'var_95': float(var_95),
                'var_99': float(var_99),
                'cvar_95': float(cvar_95),
                'max_dd_median': float(np.median(max_drawdowns)),
                'max_dd_95th': float(np.percentile(max_drawdowns, 5)),
                'probability_of_profit': probability_of_profit,
                'probability_of_ruin': probability_of_ruin,
                'percentiles': percentiles,
                'num_simulations': num_simulations,
                'days': days,
            }

            return {'status': 'success', 'data': result}

        except Exception as e:
            return {'status': 'error', 'message': str(e)}
