"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  Canopy Viz — Institutional Chart Engine                                    ║
║  Plotly-Based Dark-Theme Charts for Portfolio Analysis & Risk Reporting      ║
║                                                                              ║
║  Copyright © 2026 Anagatam Technologies. All rights reserved.               ║
╚══════════════════════════════════════════════════════════════════════════════╝

Chart Suite (9 institutional-grade interactive visualizations):
    1. Allocation Donut           — Portfolio weight distribution
    2. Dendrogram                 — Hierarchical clustering tree
    3. Quasi-Diagonal Heatmap     — Seriated correlation matrix
    4. Correlation Network        — Force-directed asset graph
    5. Cumulative Returns         — Portfolio vs benchmark performance
    6. Risk Contribution          — MCTR × weight per asset
    7. Eigenvalue Spectrum        — Signal vs noise (Marchenko-Pastur)
    8. Drawdown Chart             — Peak-to-trough decline over time
    9. Rolling Sharpe             — Time-varying risk-adjusted return

Design Philosophy:
    ─────────────────
    - Obsidian dark theme (#0a0a0a background, white text)
    - Viridis / RdBu_r colorscales (WCAG 2.1 AA color-blind accessible)
    - Inter font family (institutional standard typography)
    - All charts are interactive HTML (Plotly) — no static image dependency
    - Each function returns a go.Figure for flexible rendering

    Why Interactive HTML?
    ─────────────────────
    Static PNGs lose the hover-to-inspect capability that risk managers
    rely on for auditing individual data points. Interactive charts allow:
        - Zoom into specific time periods (drawdown analysis)
        - Hover over individual assets (weight/risk inspection)
        - Download high-res PNGs from Plotly toolbar (for presentations)
        - Embed in Jupyter notebooks or dashboards

Usage:
    >>> from canopy.viz.ChartEngine import CanopyVisualizer as cv
    >>> fig = cv.plot_allocation(weights, 'My Portfolio')
    >>> fig.show()                   # Interactive browser window
    >>> fig.write_html('out.html')   # Save for sharing
"""

# Copyright © 2026 Anagatam Technologies. All rights reserved.

import numpy as np
import pandas as pd
import plotly.graph_objects as go
import plotly.figure_factory as ff
import plotly.express as px
import networkx as nx


# ═══════════════════════════════════════════════════════════════════════════
# SHARED DARK THEME (base properties only — no axis overrides)
# ═══════════════════════════════════════════════════════════════════════════

_DARK_BG = dict(
    paper_bgcolor='rgba(10,10,10,1)',
    plot_bgcolor='rgba(10,10,10,1)',
    font=dict(color='white', family='Inter, Arial, sans-serif', size=13),
    title_font=dict(size=22, color='white'),
)

_GRID_COLOR = 'rgba(255,255,255,0.08)'
_ZERO_COLOR = 'rgba(255,255,255,0.15)'


def _dark_layout(**overrides):
    """Merge dark theme with chart-specific layout overrides."""
    layout = {**_DARK_BG, **overrides}
    return layout


class CanopyVisualizer:
    """Institutional-grade financial visualization engine.

    Copyright © 2026 Anagatam Technologies.
    """

    # ── 1. ALLOCATION DONUT ───────────────────────────────────────────────

    @staticmethod
    def plot_allocation(weights: pd.Series, title: str = 'Portfolio Allocation') -> go.Figure:
        """Donut chart of portfolio weights. Hides near-zero positions."""
        active = weights[weights.abs() > 1e-4].sort_values(ascending=False)
        fig = go.Figure(go.Pie(
            labels=active.index, values=active.values, hole=0.45,
            textinfo='label+percent',
            marker=dict(colors=px.colors.qualitative.Prism,
                       line=dict(color='#000', width=1.5))
        ))
        fig.update_layout(**_dark_layout(
            title=title, width=900, height=600,
            annotations=[dict(text=f'{len(active)}', x=0.5, y=0.5,
                             font_size=28, showarrow=False, font_color='white')],
            legend=dict(bgcolor='rgba(255,255,255,0.05)')
        ))
        return fig

    # ── 2. DENDROGRAM ─────────────────────────────────────────────────────

    @staticmethod
    def plot_dendrogram(dist_matrix: pd.DataFrame,
                        title: str = 'Hierarchical Clustering Dendrogram') -> go.Figure:
        """Dendrogram from distance matrix using Plotly's figure_factory."""
        fig = ff.create_dendrogram(
            dist_matrix.values,
            labels=dist_matrix.index.tolist()
        )
        fig.update_layout(**_dark_layout(
            title=title, width=1200, height=700,
            yaxis=dict(title='Distance', showgrid=True, gridcolor=_GRID_COLOR),
            xaxis=dict(showgrid=False),
        ))
        return fig

    # ── 3. QUASI-DIAGONAL CORRELATION HEATMAP ────────────────────────────

    @staticmethod
    def plot_correlation(correlation: pd.DataFrame, ordered_assets: list,
                         title: str = 'Quasi-Diagonalized Correlation Matrix') -> go.Figure:
        """
        Correlation heatmap reordered by the seriation (quasi-diagonalization).
        Correlated assets cluster along the diagonal — the block-diagonal
        structure reveals latent factor groups.
        """
        corr = correlation.loc[ordered_assets, ordered_assets]
        fig = go.Figure(go.Heatmap(
            z=corr.values, x=corr.columns.tolist(), y=corr.index.tolist(),
            colorscale='RdBu_r', zmid=0, zmin=-1, zmax=1,
            text=np.round(corr.values, 2), texttemplate='%{text}',
            textfont=dict(size=8)
        ))
        fig.update_layout(**_dark_layout(
            title=title, width=900, height=800,
            xaxis=dict(tickangle=45, gridcolor=_GRID_COLOR),
            yaxis=dict(gridcolor=_GRID_COLOR),
        ))
        return fig

    # ── 4. CORRELATION NETWORK ────────────────────────────────────────────

    @staticmethod
    def plot_network(correlation: pd.DataFrame, threshold: float = 0.5,
                     title: str = 'Asset Correlation Network') -> go.Figure:
        """
        Force-directed graph where edges connect assets with |ρ| > threshold.
        Node size ∝ degree (number of significant correlations).
        Highly-connected nodes are potential systemic risk concentrations.
        """
        G = nx.Graph()
        assets = correlation.columns.tolist()
        for a in assets:
            G.add_node(a)
        for i in range(len(assets)):
            for j in range(i + 1, len(assets)):
                if abs(correlation.iloc[i, j]) >= threshold:
                    G.add_edge(assets[i], assets[j], weight=correlation.iloc[i, j])

        pos = nx.spring_layout(G, seed=42, k=2)

        # Edges
        ex, ey = [], []
        for u, v in G.edges():
            x0, y0 = pos[u]; x1, y1 = pos[v]
            ex += [x0, x1, None]; ey += [y0, y1, None]
        edge_trace = go.Scatter(
            x=ex, y=ey, line=dict(width=1.5, color='rgba(150,150,150,0.4)'),
            hoverinfo='none', mode='lines'
        )

        # Nodes
        degrees = [G.degree(n) for n in G.nodes()]
        node_trace = go.Scatter(
            x=[pos[n][0] for n in G.nodes()],
            y=[pos[n][1] for n in G.nodes()],
            mode='markers+text', text=list(G.nodes()),
            textposition='top center', textfont=dict(size=11, color='white'),
            hoverinfo='text',
            marker=dict(color=degrees, colorscale='Viridis',
                       size=[15 + d * 3 for d in degrees], line_width=2,
                       colorbar=dict(title='Connections'))
        )

        fig = go.Figure(data=[edge_trace, node_trace])
        fig.update_layout(**_dark_layout(
            title=f'{title} (|ρ| > {threshold})',
            showlegend=False, width=1000, height=800,
            xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
            yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
        ))
        return fig

    # ── 5. CUMULATIVE RETURNS (Portfolio vs Benchmark) ────────────────────

    @staticmethod
    def plot_cumulative_returns(returns: pd.DataFrame, weights: pd.Series,
                                benchmark_weights: pd.Series = None,
                                title: str = 'Portfolio vs Benchmark') -> go.Figure:
        """
        Cumulative return chart comparing the optimized portfolio against
        an equal-weight benchmark.

        The gap between the two lines IS the strategy alpha — this is the
        primary chart used by quantitative portfolio managers to track
        performance attribution over time.
        """
        port_returns = (returns * weights).sum(axis=1)
        port_cum = (1 + port_returns).cumprod()

        n = len(returns.columns)
        if benchmark_weights is None:
            benchmark_weights = pd.Series(1.0 / n, index=returns.columns)
        bench_returns = (returns * benchmark_weights).sum(axis=1)
        bench_cum = (1 + bench_returns).cumprod()

        fig = go.Figure()
        fig.add_trace(go.Scatter(
            x=port_cum.index, y=port_cum.values,
            name='Canopy Portfolio', line=dict(color='#00d4aa', width=2.5)
        ))
        fig.add_trace(go.Scatter(
            x=bench_cum.index, y=bench_cum.values,
            name='Equal Weight Benchmark', line=dict(color='#ff6b6b', width=2, dash='dash')
        ))

        fig.update_layout(**_dark_layout(
            title=title, width=1200, height=600,
            xaxis=dict(title='Date', gridcolor=_GRID_COLOR),
            yaxis=dict(title='Cumulative Return (1 = initial)', gridcolor=_GRID_COLOR),
            legend=dict(x=0.01, y=0.99, bgcolor='rgba(255,255,255,0.05)'),
        ))
        return fig

    # ── 6. RISK CONTRIBUTION PER ASSET ────────────────────────────────────

    @staticmethod
    def plot_risk_contribution(weights: pd.Series, covariance: pd.DataFrame,
                               title: str = 'Marginal Risk Contribution by Asset') -> go.Figure:
        """
        Bar chart of each asset's contribution to total portfolio risk.

        Mathematical Formula:
        ─────────────────────
            MCTR_i = (Σ · w)_i / σ_p         — Marginal Contribution to Risk
            RC_i   = w_i × MCTR_i             — Risk Contribution (dollars)
            %RC_i  = RC_i / Σ RC_j × 100%    — Percentage Risk Contribution

        Interpretation:
            A perfectly risk-parity portfolio has equal %RC for all assets.
            The deviation from equal shows where risk is concentrated.
            Assets with %RC > 1.5× the equal share are highlighted in red.
        """
        w = weights.values
        cov = covariance.loc[weights.index, weights.index].values

        port_var = w @ cov @ w
        port_vol = np.sqrt(port_var)

        mctr = (cov @ w) / port_vol
        rc = w * mctr
        rc_pct = rc / rc.sum() * 100

        rc_series = pd.Series(rc_pct, index=weights.index).sort_values(ascending=True)

        equal_rc = 100 / len(w)
        colors = ['#ff6b6b' if v > equal_rc * 1.5 else '#00d4aa' for v in rc_series.values]

        fig = go.Figure(go.Bar(
            x=rc_series.values, y=rc_series.index, orientation='h',
            marker_color=colors,
            text=[f'{v:.1f}%' for v in rc_series.values],
            textposition='outside', textfont=dict(size=11)
        ))

        fig.add_vline(x=equal_rc, line_dash='dash', line_color='yellow',
                     annotation_text=f'Equal Risk ({equal_rc:.1f}%)')

        fig.update_layout(**_dark_layout(
            title=title, width=1000, height=max(500, len(w) * 28),
            xaxis=dict(title='Risk Contribution (%)', gridcolor=_GRID_COLOR),
            yaxis=dict(title='', gridcolor=_GRID_COLOR),
        ))
        return fig

    # ── 7. EIGENVALUE SPECTRUM ────────────────────────────────────────────

    @staticmethod
    def plot_eigenvalue_spectrum(covariance: pd.DataFrame, n_observations: int,
                                title: str = 'Eigenvalue Spectrum & Marchenko-Pastur Bound') -> go.Figure:
        """
        Eigenvalue bar chart with the Marchenko-Pastur (MP) random matrix
        theory noise bound overlaid.

        The Marchenko-Pastur Law (1967):
        ─────────────────────────────────
        For a random N×T matrix, the eigenvalue density follows:

            f(λ) = (T/N) · √((λ₊ - λ)(λ - λ₋)) / (2π·σ²·λ)

        where:
            λ₊ = σ²·(1 + √(N/T))²    — upper bound for noise
            λ₋ = σ²·(1 - √(N/T))²    — lower bound for noise

        Interpretation:
            Eigenvalues ABOVE λ₊ carry economic signal (factor structure).
            Eigenvalues BELOW λ₊ are sampling noise.
            The number of signal eigenvalues ≈ number of latent risk factors.
        """
        cov = covariance.values
        n_assets = cov.shape[0]

        eigenvalues = np.sort(np.linalg.eigvalsh(cov))[::-1]

        sigma_sq = np.mean(np.diag(cov))
        q = n_assets / n_observations
        mp_upper = sigma_sq * (1 + np.sqrt(q)) ** 2

        n_signal = int(np.sum(eigenvalues > mp_upper))
        colors = ['#00d4aa' if ev > mp_upper else '#555555' for ev in eigenvalues]

        fig = go.Figure()
        fig.add_trace(go.Bar(
            x=[f'λ{i+1}' for i in range(len(eigenvalues))],
            y=eigenvalues, marker_color=colors,
            name='Eigenvalues',
            text=[f'{ev:.2e}' for ev in eigenvalues],
            textposition='outside', textfont=dict(size=9)
        ))

        fig.add_hline(y=mp_upper, line_dash='dash', line_color='#ffe66d',
                     annotation_text=f'MP Upper Bound (λ₊ = {mp_upper:.2e})',
                     annotation_font_color='#ffe66d')

        fig.update_layout(**_dark_layout(
            title=f'{title}<br><sub>{n_signal} signal eigenvalues, '
                  f'{n_assets - n_signal} noise eigenvalues (T/N = {n_observations/n_assets:.1f})</sub>',
            width=1100, height=600,
            xaxis=dict(title='Eigenvalue Index', gridcolor=_GRID_COLOR),
            yaxis=dict(title='Eigenvalue Magnitude', type='log', gridcolor=_GRID_COLOR),
        ))
        return fig

    # ── 8. DRAWDOWN CHART ─────────────────────────────────────────────────

    @staticmethod
    def plot_drawdown(returns: pd.DataFrame, weights: pd.Series,
                      title: str = 'Portfolio Drawdown') -> go.Figure:
        """
        Underwater equity curve showing peak-to-trough decline over time.

        Formula:
            DD_t = (Cumulative_t − Peak_t) / Peak_t

        Max drawdown is the single most important risk metric for
        institutional investors — it measures the worst-case loss experience
        and directly impacts fund redemptions and margin calls.
        """
        port_returns = (returns * weights).sum(axis=1)
        cumulative = (1 + port_returns).cumprod()
        peak = cumulative.cummax()
        drawdown = (cumulative - peak) / peak * 100

        fig = go.Figure()
        fig.add_trace(go.Scatter(
            x=drawdown.index, y=drawdown.values,
            fill='tozeroy', fillcolor='rgba(255,107,107,0.3)',
            line=dict(color='#ff6b6b', width=1.5),
            name='Drawdown'
        ))

        max_dd_idx = drawdown.idxmin()
        max_dd_val = drawdown.min()
        fig.add_annotation(
            x=max_dd_idx, y=max_dd_val,
            text=f'Max DD: {max_dd_val:.1f}%',
            showarrow=True, arrowcolor='white', font=dict(color='white', size=13)
        )

        fig.update_layout(**_dark_layout(
            title=title, width=1200, height=500,
            xaxis=dict(title='Date', gridcolor=_GRID_COLOR),
            yaxis=dict(title='Drawdown (%)', gridcolor=_GRID_COLOR),
        ))
        return fig

    # ── 9. ROLLING SHARPE RATIO ───────────────────────────────────────────

    @staticmethod
    def plot_rolling_sharpe(returns: pd.DataFrame, weights: pd.Series,
                            window: int = 63,
                            title: str = 'Rolling Sharpe Ratio (63-day)') -> go.Figure:
        """
        Rolling Sharpe ratio over a sliding window.

        Formula:
            Sharpe_t = mean(r_{t-w..t}) / std(r_{t-w..t}) × √252

        Default window: 63 trading days (≈ 1 quarter).
        Shows how risk-adjusted returns evolve over time — critical for
        identifying correlation regime changes and strategy decay.
        """
        port_returns = (returns * weights).sum(axis=1)
        rolling_mean = port_returns.rolling(window).mean()
        rolling_std = port_returns.rolling(window).std()
        rolling_sharpe = (rolling_mean / rolling_std) * np.sqrt(252)
        rolling_sharpe = rolling_sharpe.dropna()

        fig = go.Figure()
        fig.add_trace(go.Scatter(
            x=rolling_sharpe.index, y=rolling_sharpe.values,
            line=dict(color='#4ecdc4', width=2),
            name=f'{window}-day Rolling Sharpe'
        ))

        fig.add_hline(y=0, line_dash='solid', line_color='rgba(255,255,255,0.3)')
        fig.add_hline(y=1, line_dash='dash', line_color='#ffe66d',
                     annotation_text='Sharpe = 1.0')
        fig.add_hline(y=2, line_dash='dash', line_color='#00d4aa',
                     annotation_text='Sharpe = 2.0')

        fig.add_hrect(y0=-10, y1=0, fillcolor='rgba(255,107,107,0.05)', line_width=0)
        fig.add_hrect(y0=0, y1=10, fillcolor='rgba(0,212,170,0.03)', line_width=0)

        fig.update_layout(**_dark_layout(
            title=title, width=1200, height=500,
            xaxis=dict(title='Date', gridcolor=_GRID_COLOR),
            yaxis=dict(title='Sharpe Ratio (annualized)',
                      range=[rolling_sharpe.min() - 0.5, rolling_sharpe.max() + 0.5],
                      gridcolor=_GRID_COLOR),
        ))
        return fig
