import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import math
from sklearn.linear_model import LinearRegression
from matplotlib.widgets import Button


class Horner:
    """
    Class to perform Horner analysis on pressure buildup data.
    """

    def __init__(self, tp=None, filePath=None):
        """
        Initializes the Horner object.

        Args:
            tp (float): Shut-in time.
            filePath (str): Path to the CSV file containing pressure buildup data.
        """
        self.tp = tp
        self.filePath = filePath
        if self.filePath is not None:
            self.p_column, self.t_column = self.name_column_csv()
            self.df = pd.read_csv(self.filePath)
            self.validate_data()
            self.p = self.df[self.p_column]
            self.t = self.df[self.t_column]
        else:
            raise ValueError("Invalid file path. Please provide a valid file path.")

    def name_column_csv(self):
        """
        Returns the default column names for pressure and time in the CSV file.

        Returns:
            tuple: Tuple containing the pressure column name and time column name.
        """
        p_column = "p"
        t_column = "t"
        return p_column, t_column

    def validate_data(self):
        """
        Validates that the CSV file contains the required columns.
        """
        required_columns = [self.p_column, self.t_column]
        if not all(column in self.df.columns for column in required_columns):
            raise ValueError(f"CSV file must contain columns: {required_columns}")

    def horner_plot(self):
        """
        Generates the Horner plot data.

        Returns:
            tuple: Tuple containing the Horner time ratio list and pressure list.
        """
        if self.tp is None:
            raise ValueError("Please provide a shut-in time (tp).")

        horner_list = [
            (self.tp + self.t[i]) / self.t[i]
            for i in range(len(self.t))
            if self.t[i] != 0
        ]
        return horner_list, self.p[1:]


class BuildUpCalculatorParameter(Horner):
    """
    Class to calculate permeability and skin factor from Horner analysis.
    """

    def __init__(
        self,
        filePath=None,
        tp=None,
        Qo=None,
        Bo=None,
        mo=None,
        Ø=None,
        Ct=None,
        rw=None,
        h=None,
    ):
        """
        Initializes the BuildUpCalculatorParameter object.

        Args:
            filePath (str): Path to the CSV file containing pressure buildup data.
            tp (float): Shut-in time.
            Qo (float): Oil flow rate (bbl/day).
            Bo (float): Oil formation volume factor (bbl/STB).
            mo (float): Oil viscosity (cP).
            Ø (float): Porosity.
            Ct (float): Total compressibility (1/psi).
            rw (float): Wellbore radius (ft).
            h (float): Formation thickness (ft).
        """
        super().__init__(tp=tp, filePath=filePath)
        self.Qo = Qo
        self.Bo = Bo
        self.mo = mo
        self.Ø = Ø
        self.Ct = Ct
        self.rw = rw
        self.h = h

    @property
    def Ph0(self):
        """
        Returns the initial pressure at shut-in.

        Returns:
            float: Initial pressure (psi).
        """
        return self.p[0]

    def Ph1(self, slope=None, intercept=None):
        """
        Calculates the pressure at 1 hour after shut-in.

        Args:
            slope (float): Slope of the Horner plot.
            intercept (float): Intercept of the Horner plot.

        Returns:
            float: Pressure at 1 hour after shut-in (psi).
        """
        if slope is None or intercept is None:
            raise ValueError("Slope and intercept must be provided.")
        return intercept + (slope * math.log(self.tp + 1))

    def k(self, slope):
        """
        Calculates the permeability.

        Args:
            slope (float): Slope of the Horner plot.

        Returns:
            float: Permeability (mD).
        """
        _K_CONSTANT = 162.6  # Constant for calculating permeability
        if self.Qo is None or self.Bo is None or self.mo is None or self.h is None:
            raise ValueError("Qo, Bo, mo, and h must be provided.")
        kh = _K_CONSTANT * self.Qo * self.Bo * self.mo / slope
        return abs(kh / self.h)

    def skin(self, intercept, slope, k):
        """
        Calculates the skin factor.

        Args:
            intercept (float): Intercept of the Horner plot.
            slope (float): Slope of the Horner plot.
            k (float): Permeability (mD).

        Returns:
            float: Skin factor.
        """
        P1hr = self.Ph1(slope=slope, intercept=intercept)
        if self.Ø is None or self.mo is None or self.Ct is None or self.rw is None:
            raise ValueError("Ø, mo, Ct, and rw must be provided.")
        return (
            (1.151 * ((P1hr - self.Ph0) / slope))
            - math.log10((k / (self.Ø * self.mo * self.Ct * self.rw**2)))
            + 3.23
        )


class InteractivePlot(BuildUpCalculatorParameter):
    """
    Class for interactive Horner plot with linear regression and results display.
    """

    def __init__(
        self,
        fig,
        ax,
        filePath=None,
        tp=None,
        Qo=None,
        Bo=None,
        mo=None,
        Ø=None,
        Ct=None,
        rw=None,
        h=None,
    ):
        """
        Initializes the InteractivePlot object.

        Args:
            fig (matplotlib.figure.Figure): Matplotlib figure object.
            ax (matplotlib.axes.Axes): Matplotlib axes object.
            filePath (str): Path to the CSV file containing pressure buildup data.
            tp (float): Shut-in time.
            Qo (float): Oil flow rate (bbl/day).
            Bo (float): Oil formation volume factor (bbl/STB).
            mo (float): Oil viscosity (cP).
            Ø (float): Porosity.
            Ct (float): Total compressibility (1/psi).
            rw (float): Wellbore radius (ft).
            h (float): Formation thickness (ft).
        """
        super().__init__(
            filePath=filePath, tp=tp, Qo=Qo, Bo=Bo, mo=mo, Ø=Ø, Ct=Ct, rw=rw, h=h
        )
        self.fig = fig
        self.ax = ax
        self.points = []
        self.line = None
        self.cid = fig.canvas.mpl_connect("button_press_event", self.on_click)
        self.horner_list, self.pressure_list = self.horner_plot()
        self.create_plot()

        # Add reset button
        axreset = plt.axes([0.8, 0.05, 0.1, 0.04])
        self.button = Button(axreset, "Reset")
        self.button.on_clicked(self.reset_plot)

    def create_plot(self):
        """
        Creates the initial Horner plot with axes labels and grid.
        """
        self.ax.set_title("Horner Plot")
        self.ax.set_xlabel("Horner Time Ratio (tp + t) / t")
        self.ax.set_ylabel("Pressure (psi)")
        self.ax.grid(True, which="both", linestyle="--")
        self.ax.semilogx(
            self.horner_list, self.pressure_list, marker="o", ls="", color="k", ms=4
        )

    def fit_linear_model(self, x, y):
        """
        Fits a linear regression model to the selected points.

        Args:
            x (list): List of x-coordinates of selected points.
            y (list): List of y-coordinates of selected points.

        Returns:
            tuple: Tuple containing the slope, intercept, and predicted y values.
        """
        x = np.array(x)
        y = np.array(y)
        x_reshaped = x.reshape(-1, 1)
        model = LinearRegression()
        model.fit(x_reshaped, y)
        y_pred = model.predict(x_reshaped)
        slope = model.coef_[0]
        intercept = model.intercept_
        return slope, intercept, y_pred

    def on_click(self, event):
        """
        Handles mouse click events on the plot.
        """
        if event.inaxes != self.ax:
            return

        # Find the nearest data point
        click_point = (event.xdata, event.ydata)
        nearest_point = self.find_nearest_point(click_point)

        if nearest_point:
            self.points.append(nearest_point)
            self.ax.plot(nearest_point[0], nearest_point[1], marker="x", color="red")

            if len(self.points) == 2:
                start_index_xas = self.horner_list.index(self.points[0][0])
                end_index_xas = self.horner_list.index(self.points[1][0])
                a2 = []
                a2.append(start_index_xas)
                a2.append(end_index_xas)
                start_index_xas, end_index_xas = self.inexss(a2)
                xas = [
                    np.log(i)
                    for i in self.horner_list[start_index_xas : end_index_xas + 1]
                ]
                lest_of_preeser = [i for i in self.pressure_list]
                start_index_yas = lest_of_preeser.index(self.points[0][1])
                end_index_yas = lest_of_preeser.index(self.points[1][1])
                a1 = []
                a1.append(start_index_yas)
                a1.append(end_index_yas)
                start_index_yas, end_index_yas = self.inexss(a1)

                yas = lest_of_preeser[start_index_yas : end_index_yas + 1]

                slope, intercept, _ = self.fit_linear_model(xas, yas)

                self.ax.plot(
                    self.horner_list,
                    intercept + slope * np.log(self.horner_list),
                    color="blue",
                )

                # Highlight the data points used for slope calculation
                for point in self.points:
                    self.ax.plot(point[0], point[1], marker="x", color="red", ms=10)

                plt.draw()
                self.display_results(slope, intercept)

    def inexss(self, a: list):
        """
        Returns the minimum and maximum values in a list.
        """
        amax = np.max(a)
        amin = np.min(a)
        start = amin
        end = amax
        return start, end

    def find_nearest_point(self, click_point):
        """
        Finds the nearest data point to the click point.
        """
        distances = [
            (hx, hp, self.euclidean_distance(click_point, (hx, hp)))
            for hx, hp in zip(self.horner_list, self.pressure_list)
        ]
        nearest = min(distances, key=lambda t: t[2])
        return (nearest[0], nearest[1])

    @staticmethod
    def euclidean_distance(point1, point2):
        """
        Calculates the Euclidean distance between two points.
        """
        return np.sqrt((point1[0] - point2[0]) ** 2 + (point1[1] - point2[1]) ** 2)

    def display_results(self, slope, intercept):
        """
        Displays the calculated permeability and skin factor.
        """
        k = self.k(slope)
        s = self.skin(intercept, abs(slope), k)
        print(f"Permeability (k): {k:.4f} mD")
        print(f"Skin factor (s): {s:.4f}")
        # Display results on the plot
        textstr = f"Permeability (k): {k:.4f} mD\nSkin factor (s): {s:.4f}"
        props = dict(boxstyle="round", facecolor="wheat", alpha=0.5)
        self.ax.text(
            0.10,
            0.20,
            textstr,
            transform=self.ax.transAxes,
            fontsize=14,
            verticalalignment="top",
            bbox=props,
        )

    def reset_plot(self, event):
        """
        Resets the plot to its initial state.
        """
        # Clear the selected points and reset the plot
        self.points = []
        self.ax.clear()
        self.create_plot()  # Replot the initial Horner plot
        plt.draw()


# Example usage
if __name__ == "__main__":

    fil = r"C:\Users\pc\Desktop\projects\oil_and_gas\worked_data.csv"
    fig, ax = plt.subplots()
    interactive_plot = InteractivePlot(
        fig,
        ax,
        filePath=fil,
        tp=15724.26,
        Qo=542,
        Bo=2.5052,
        mo=0.108,
        Ø=0.031,
        Ct=3.91e-5,
        rw=0.245,
        h=787,
    )
    plt.show()
Make this code written in a professional manner