from pptx.enum.text import PP_ALIGN  # NOQA
from pptx import Presentation
from pptx.util import Inches, Pt
import matplotlib.pyplot as plt
from sqlalchemy.orm import Session
from sqlalchemy.sql import true
from backend.app.db.utils import row2dict
from backend.app.db.models.portfolio import PropertyModel, PropertyDetailsEpcModel
from backend.app.db.models.recommendations import Recommendation
from backend.app.db.models.recommendations import PlanModel
from backend.app.utils import sap_to_epc

EPC_COLOURS = {
    "A": "#028051",
    "B": "#14b759",
    "C": "#8ecd46",
    "D": "#fdd401",
    "E": "#fdab67",
    "F": "#ee8023",
    "G": "#e71437",
}


def get_properties_with_default_recommendations(session: Session, portfolio_id: int):
    """
    Fetch properties for a given portfolio_id along with their default recommendations,
    ensuring that all properties are retrieved even if they don't have recommendations
    where default is True.

    :param session: The SQLAlchemy session used to execute the query.
    :param portfolio_id: The ID of the portfolio for which to retrieve properties and recommendations.
    :return: A list of dictionaries, where each dictionary represents a property including
             its associated default recommendations if any.
    """
    # Adjust the join to correctly filter recommendations while including all properties
    query = (
        session.query(PropertyModel, Recommendation)
        .outerjoin(
            Recommendation,
            (Recommendation.property_id == PropertyModel.id)
            & (Recommendation.default == true()),
        )
        .filter(PropertyModel.portfolio_id == portfolio_id)
        .all()
    )

    properties = {}
    for property, recommendation in query:
        # Ensure the property is added once with an empty list of recommendations initially
        if property.id not in properties:
            properties[property.id] = row2dict(property)
            properties[property.id]["recommendations"] = []

        # Append recommendations if they exist and meet the criteria (already filtered by the query)
        if recommendation and recommendation.default:
            properties[property.id]["recommendations"].append(row2dict(recommendation))

    return list(properties.values())


def get_property_details_by_portfolio_id(session: Session, portfolio_id: int):
    """
    This function retrieves all property details associated with a given portfolio_id.

    :param session: The SQLAlchemy session used to execute the query.
    :param portfolio_id: The ID of the portfolio for which to retrieve property details.
    :return: A list of dictionaries, where each dictionary represents a property's details.
             Returns an empty list if no property details are found.
    """
    property_details = (
        session.query(PropertyDetailsEpcModel)
        .filter(PropertyDetailsEpcModel.portfolio_id == portfolio_id)
        .all()
    )

    # Convert the SQLAlchemy objects to dictionaries
    property_details_dict = (
        [row2dict(pd) for pd in property_details] if property_details else []
    )

    return property_details_dict


def get_plan_by_portfolio_id(session: Session, portfolio_id: int):
    """
    This function retrieves all plans associated with a given portfolio_id.

    :param session: The SQLAlchemy session used to execute the query.
    :param portfolio_id: The ID of the portfolio for which to retrieve plans.
    :return: A list of dictionaries, where each dictionary represents a plan.
             Returns an empty list if no plans are found.
    """
    plans = (
        session.query(PlanModel).filter(PlanModel.portfolio_id == portfolio_id).all()
    )

    # Convert the SQLAlchemy objects to dictionaries
    plans_dict = [row2dict(plan) for plan in plans] if plans else []

    return plans_dict


def plot_epc_distribution(
    df,
    customer_key,
    title="Your Units",
    background_color="white",
    bar_height=0.4,
    font_size=15,
):
    """
    Plots a horizontal bar chart of EPC rating distribution with adjustable bar thickness and text sizes.
    Allows setting the plot background color and dynamically adjusts text size and bar spacing.

    :param df: DataFrame with columns ['current_epc_rating', 'count', 'percentage']
    :param title: Title of the plot
    :param background_color: Background color of the plot
    :param bar_height: Thickness of the bars (default 0.4)
    :param font_size: Base font size for text annotations (default 15)
    """
    # Calculate dynamic figure size or adjust based on preferences
    square_size = max(
        6, len(df) * 0.6
    )  # Ensure minimum size and adjust based on number of entries
    fig, ax = plt.subplots(figsize=(square_size, square_size))
    fig.patch.set_facecolor(background_color)  # Set figure background color
    ax.set_facecolor(background_color)  # Set axes background color

    df["percentage"] = df["percentage"].round(
        1
    )  # Round the percentage values to 1 decimal place
    df_sorted = df.sort_values("percentage", ascending=True)

    # Plot bars with specified height for adjustable thickness
    bars = ax.barh(
        df_sorted["current_epc_rating"],
        df_sorted["percentage"],
        color=df_sorted["current_epc_rating"].map(EPC_COLOURS),
        edgecolor="none",
        height=bar_height,
    )

    epc_rating_font_size = (
        font_size * 2
    )  # EPC rating font size larger than base font size
    count_percentage_font_size = (
        font_size  # Count (percentage) font size as base font size
    )

    # Annotate bars with EPC ratings inside and count with percentage values outside
    for index, bar in enumerate(bars):
        width = bar.get_width()
        epc_rating = df_sorted.iloc[index]["current_epc_rating"]
        count = df_sorted.iloc[index]["count"]
        percentage = df_sorted.iloc[index]["percentage"]

        # EPC rating inside the bar with increased font size
        ax.text(
            width - (width * 0.05),
            bar.get_y() + bar.get_height() / 2,
            f"{epc_rating}",
            va="center",
            ha="right",
            color="white",
            fontsize=epc_rating_font_size,
        )

        # Count and percentage outside the bar, original font size
        ax.text(
            width + 1,
            bar.get_y() + bar.get_height() / 2,
            f"{count} ({percentage}%)",
            va="center",
            color="black",
            fontsize=count_percentage_font_size,
        )

    ax.set_title(
        title, fontsize=font_size * 1.2
    )  # Adjust title font size proportionally
    ax.tick_params(
        axis="x", which="both", bottom=False, top=False, labelbottom=False
    )  # Remove x-axis tick marks and values
    ax.tick_params(
        axis="y", which="both", left=False, right=False, labelleft=False
    )  # Remove y-axis tick marks and labels
    ax.spines["top"].set_visible(False)  # Remove top spine
    ax.spines["right"].set_visible(False)  # Remove right spine
    ax.spines["left"].set_visible(False)  # Remove left spine
    ax.spines["bottom"].set_visible(False)  # Remove bottom spine

    plt.tight_layout()  # Adjust layout
    plt.show()

    # Save the figure as an image
    figure_path = f"etl/customers/{customer_key}/epc_distribution_plot.png"
    fig.savefig(figure_path, bbox_inches="tight")
    plt.close(fig)  # Close the figure to free memory

    return fig, figure_path


def save_plot_to_image(figure, path="plot.png"):
    """
    Saves a matplotlib figure to an image file for insertion into PowerPoint.
    """
    figure.savefig(path, bbox_inches="tight")
    plt.close(figure)


def save_figure_as_image(figure, filename="temp_plot.png"):
    """
    Saves a matplotlib figure to an image file.
    """
    figure.savefig(filename, dpi=300)
    plt.close(
        figure
    )  # Close the figure to prevent it from displaying in notebooks or Python environments


def add_commentary_with_bullets(
    slide,
    commentary,
    top_inches,
    left_inches=Inches(1),
    width_inches=Inches(8),
    height_inches=Inches(2),
):
    """
    Adds commentary with bullet points to a slide.

    :param slide: The slide object to add the commentary to.
    :param commentary: The commentary text, with sections separated by newlines for bullet points.
    :param top_inches: The top position of the commentary text box.
    :param left_inches: The left position of the commentary text box.
    :param width_inches: The width of the commentary text box.
    :param height_inches: The height of the commentary text box.
    """
    txBox = slide.shapes.add_textbox(
        left_inches, top_inches, width_inches, height_inches
    )
    tf = txBox.text_frame

    # Configure text frame
    tf.word_wrap = True
    tf.auto_size = True
    tf.paragraphs[0].alignment = PP_ALIGN.LEFT

    # Split the commentary into sections for bullet points
    sections = commentary.split("\n")

    for i, section in enumerate(sections):
        if i > 0:
            p = (
                tf.add_paragraph()
            )  # Add a new paragraph for each section after the first
        else:
            p = tf.paragraphs[0]  # Use the first paragraph for the first section
        p.text = section
        p.space_after = Pt(14)  # Adjust space after each bullet point as needed
        p.font.size = Pt(14)  # Adjust font size as needed
        p.level = 0  # Bullet level, can be adjusted for nested bullets
        p.space_before = Pt(0)


def add_slide_with_image(prs, title, img_path=None, commentary=None):
    """
    Adds a slide with an image (if provided) and optional commentary. If no image is provided,
    places the commentary text in the middle of the slide.
    """
    slide_layout = prs.slide_layouts[5]  # Title and Content layout
    slide = prs.slides.add_slide(slide_layout)
    title_placeholder = slide.shapes.title
    title_placeholder.text = title

    # Determine the position of the commentary text box based on whether an image is included
    if img_path:
        # Add the image
        slide.shapes.add_picture(
            img_path, Inches(1), Inches(1.5), Inches(8), Inches(4.5)
        )
        # Position for commentary when image is present
        commentary_top = Inches(6)
    else:
        # Position for commentary when image is not present (centered vertically)
        commentary_top = Inches(3)

    # Add commentary if provided
    if commentary:
        add_commentary_with_bullets(slide, commentary, commentary_top)


def create_powerpoint(data, save_location):
    """
    Creates a PowerPoint presentation based on provided data and optional commentaries.

    :param data: A dictionary containing the data needed for each slide.
    :param save_location: The file path where the PowerPoint presentation will be saved.
    """
    prs = Presentation()

    for slide, slide_data in data.items():
        slide_figure_path = data[slide].get("image_path")
        text = data[slide].get("text")
        title = data[slide].get("title", "")
        add_slide_with_image(prs, title, slide_figure_path, text)

    # Save the presentation
    prs.save(save_location)


def create_recommendations_summary(
    recommendations_df, properties_df, property_details_df, sap_target
):
    # Aggregate the impact of the recommendations
    # We want:
    # Total number of sap points
    # total valuation impact
    # total bill savings
    # total cost
    # Total Co2 impact
    recommendations_summary = (
        recommendations_df.groupby(["property_id"])
        .agg(
            total_sap_points=("sap_points", "sum"),
            total_valuation_impact=("property_valuation_increase", "sum"),
            total_bill_savings=("energy_cost_savings", "sum"),
            total_cost=("estimated_cost", "sum"),
            total_carbon=("co2_equivalent_savings", "sum"),
            adjusted_heat_demand=("adjusted_heat_demand", "sum"),
        )
        .reset_index()
    )
    # Merge on current sap points, current CO2, current adjusted_heat_demand, current annual bill
    recommendations_summary = recommendations_summary.merge(
        properties_df[["id", "uprn", "current_sap_points"]].rename(
            columns={"id": "property_id"}
        ),
        on="property_id",
        how="left",
    )

    recommendations_summary["expected_sap_points"] = (
        recommendations_summary["current_sap_points"]
        + recommendations_summary["total_sap_points"]
    )
    recommendations_summary["expected_epc_rating"] = recommendations_summary[
        "expected_sap_points"
    ].apply(lambda x: sap_to_epc(x))
    recommendations_summary["sap_difference"] = (
        sap_target - recommendations_summary["expected_sap_points"]
    )

    if property_details_df is not None:
        recommendations_summary = recommendations_summary.merge(
            property_details_df[
                ["uprn", "co2_emissions", "adjusted_energy_consumption", "energy_bill"]
            ].rename(
                columns={
                    "id": "property_id",
                    "co2_emissions": "current_co2",
                    "adjusted_energy_consumption": "current_energy",
                    "energy_bill": "current_energy_bill",
                }
            ),
            on="uprn",
            how="left",
        )

    return recommendations_summary
