import requests
import csv
import math
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import mplcursors  # برای tooltip interactive
import matplotlib.collections as mcoll
import numpy as np
import itertools
import sys
import time
import matplotlib.dates as mdates
import mplfinance as mpf  # اضافه کن در ابتدای فایل
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry


class MarketRecord:
    currency_map = {
        "dollar": "price_dollar_rl",
        "euro": "price_eur",
        "pound": "price_gbp",
        "aed": "price_aed",
        "gold": "geram18",
    }

    LIB_NAME = "pahlavi"

    def __init__(self, total=20, page_size=30):
        self.total = total
        self.page_size = page_size
        self._last_data = []
        self._last_currency = None

    def _get_data(
        self,
        currency,
        csv_save=False,
        filename=None,
        max_retries=4,
        page_sleep=0.3,
        timeout=10,
    ):
        """
        Robust data fetcher with session+retries, SSL handling, progress animation.
        Parameters:
        - currency: api path piece, e.g. "price_eur"
        - csv_save, filename: same as before
        - max_retries: per-request retry attempts (in addition to urllib3 retry policy)
        - page_sleep: seconds to sleep between page requests
        - timeout: request timeout in seconds
        """
        if csv_save and not filename:
            raise ValueError("When csv_save=True, you must provide a filename!")

        base_url = f"https://api.tgju.org/v1/market/indicator/today-table-data/{currency}?lang=fa"
        all_data = []
        pages = math.ceil(self.total / self.page_size)
        total_items = self.total
        fetched_items = 0

        # build Session with Retry adapter
        from requests.adapters import HTTPAdapter
        from urllib3.util.retry import Retry

        session = requests.Session()
        retries = Retry(
            total=max_retries,
            backoff_factor=0.5,
            status_forcelist=(429, 500, 502, 503, 504),
            allowed_methods=frozenset(["GET", "HEAD", "OPTIONS"]),
        )
        adapter = HTTPAdapter(max_retries=retries, pool_connections=10, pool_maxsize=10)
        session.mount("https://", adapter)
        session.headers.update(
            {
                "User-Agent": f"{self.LIB_NAME}/0.x (+https://github.com/yourname/{self.LIB_NAME})"
            }
        )

        animation_chars = ["-", "\\", "|", "/"]
        dots = ["   ", ".  ", ".. ", "..."]
        text_wave = itertools.cycle([0, 1, 2, 1])
        bar_len = 30
        anim_index = 0

        print("\nFetching data...\n")

        stop_early = False
        for start in range(0, pages * self.page_size, self.page_size):
            params = {"start": start, "length": self.page_size}
            attempt = 0
            while True:
                attempt += 1
                try:
                    res = session.get(base_url, params=params, timeout=timeout)
                    res.raise_for_status()
                    try:
                        j = res.json()
                    except ValueError:
                        # اگر JSON خراب بود retry کن
                        raise requests.exceptions.RequestException(
                            "Invalid JSON response"
                        )
                    break
                except requests.exceptions.SSLError as e:
                    # SSL ممکنه توسط سرور قطع شده باشه — کمی بیشتر صبر کن و retry
                    wait = min(5 + attempt * 2, 30)
                    sys.stdout.write(
                        f"\n[SSL error] attempt {attempt}/{max_retries}. wait {wait}s...\n"
                    )
                    sys.stdout.flush()
                    time.sleep(wait)
                    if attempt >= max_retries:
                        raise
                    continue
                except requests.exceptions.RequestException as e:
                    # شامل timeout, connection error, HTTPError و غیره
                    wait = min(0.5 * (2 ** (attempt - 1)), 10)
                    sys.stdout.write(
                        f"\n[Request error] attempt {attempt}/{max_retries}. backoff {wait:.1f}s ({e})\n"
                    )
                    sys.stdout.flush()
                    time.sleep(wait)
                    if attempt >= max_retries:
                        raise
                    continue

            rows = j.get("data", [])
            if not rows:
                # اگر API داده‌ای برنگردوند، احتمالاً دیگه صفحه‌ای نیست
                sys.stdout.write("\nNo more data returned by API -> stopping early.\n")
                stop_early = True
                break

            for row in rows:
                # تبدیل امن قیمت و نادیده گرفتن ردیف‌های خراب
                try:
                    price_raw = str(row[0]).strip()
                    price = int(price_raw.replace(",", ""))
                except Exception:
                    # skip malformed row
                    continue

                time_val = row[1]
                all_data.append((price, time_val))
                fetched_items += 1

                # progress bar update (animation preserved)
                filled_len = int(round(bar_len * fetched_items / float(total_items)))
                percents = round(100.0 * fetched_items / float(total_items), 1)
                bar = "#" * filled_len + "-" * (bar_len - filled_len)
                anim_char = animation_chars[anim_index % len(animation_chars)]
                dot = dots[anim_index % len(dots)]
                wave = " " * next(text_wave)
                anim_index += 1

                sys.stdout.write(
                    f"\r{wave}pahlavi is working{dot} [{bar}] {percents}% {anim_char}"
                )
                sys.stdout.flush()

                # امن کردن سرعت چاپ
                time.sleep(0.02)

                if fetched_items >= total_items:
                    break

            if fetched_items >= total_items or stop_early:
                break

            # فاصله بین صفحات تا فشار روی API کم باشد
            time.sleep(page_sleep)

        sys.stdout.write("\nDone\n\n")

        self._last_data = all_data
        self._last_currency = currency

        if csv_save:
            self._to_csv(all_data, filename)

        return self

    def _to_csv(self, data, filename):
        with open(filename, "w", newline="", encoding="utf-8") as f:
            writer = csv.writer(f)
            writer.writerow(["Price", "Time"])
            writer.writerows(data)
            writer.writerow([])
            writer.writerow(
                [f"by: {self.LIB_NAME}", f"pip install {self.LIB_NAME.lower()}"]
            )
        print(f"\nData saved to {filename}")

    # currency methods
    def dollar(self, csv_save=False, filename=None):
        return self._get_data(self.currency_map["dollar"], csv_save, filename)

    def euro(self, csv_save=False, filename=None):
        return self._get_data(self.currency_map["euro"], csv_save, filename)

    def pound(self, csv_save=False, filename=None):
        return self._get_data(self.currency_map["pound"], csv_save, filename)

    def aed(self, csv_save=False, filename=None):
        return self._get_data(self.currency_map["aed"], csv_save, filename)

    def gold(self, csv_save=False, filename=None):
        return self._get_data(self.currency_map["gold"], csv_save, filename)

    def plot(
        self, in_toman=True, save=False, filename="market_plot.png", chart_type="line"
    ):
        """
        chart_type: "line" (default), "bar", "candlestick"
        """
        if not self._last_data:
            print("No data to plot. Fetch data first.")
            return

        prices, times = zip(*self._last_data)
        if in_toman:
            prices = [p // 10 for p in prices]

        currency_name = next(
            (k for k, v in self.currency_map.items() if v == self._last_currency),
            "Unknown",
        )

        if chart_type == "line":
            # همان کد plot خطی قبل
            plt.style.use("dark_background")
            fig, ax = plt.subplots(figsize=(18, 6))
            x = np.arange(len(times))
            segments = [
                [(x[i - 1], prices[i - 1]), (x[i], prices[i])]
                for i in range(1, len(prices))
            ]
            colors = [
                "#00ff00" if prices[i] >= prices[i - 1] else "#ff4c4c"
                for i in range(1, len(prices))
            ]
            lc = mcoll.LineCollection(segments, colors=colors, linewidths=2)
            ax.add_collection(lc)
            scatter = ax.scatter(
                x, prices, color="#00ffff", edgecolors="white", s=50, zorder=3
            )
            step = max(1, len(times) // 20)
            ax.set_xticks(range(0, len(times), step))
            ax.set_xticklabels(
                [times[i] for i in range(0, len(times), step)],
                rotation=45,
                fontsize=10,
                color="white",
            )
            ax.tick_params(axis="y", labelcolor="white")
            ax.set_xlabel("Time", fontsize=12, color="white")
            ax.set_ylabel(
                "Price (Toman)" if in_toman else "Price (Rial)",
                fontsize=12,
                color="white",
            )
            ax.set_title(
                f"{currency_name.capitalize()} Market Data",
                fontsize=16,
                color="white",
                fontweight="bold",
            )
            fig.text(
                0.95,
                0.01,
                f"Created by: Pahlavi",
                fontsize=10,
                color="white",
                ha="right",
                va="bottom",
            )
            ax.grid(color="gray", linestyle="--", linewidth=0.5, alpha=0.7)
            ax.yaxis.set_major_formatter(mticker.StrMethodFormatter("{x:,.0f}"))
            plt.tight_layout()
            cursor = mplcursors.cursor([scatter, lc], hover=True)

            @cursor.connect("add")
            def on_add(sel):
                xdata, ydata = sel.target
                idx = int(np.argmin(np.abs(np.array(x) - xdata)))
                sel.annotation.set_text(f"Time: {times[idx]}\nPrice: {prices[idx]:,}")
                sel.annotation.get_bbox_patch().set(fc="black", alpha=0.8)

            if save:
                plt.savefig(filename, dpi=300)
                print(f"Plot saved to {filename}")
            plt.show()
            return list(zip(prices, times))

        elif chart_type == "candlestick":
            import pandas as pd
            import re

            # build DataFrame and keep raw_time for parsing
            df = pd.DataFrame({"Close": prices, "raw_time": times})

            # try fast parse for pure time-only strings (HH:MM or HH:MM:SS)
            strs = df["raw_time"].astype(str)
            time_only_mask = strs.str.match(r"^\s*\d{1,2}:\d{2}(:\d{2})?\s*$")

            if time_only_mask.all():
                # if all rows are time-only, parse explicitly to avoid infer warnings
                # first try seconds format, then minute-only
                parsed = pd.to_datetime(strs, format="%H:%M:%S", errors="coerce")
                if parsed.isna().all():
                    parsed = pd.to_datetime(strs, format="%H:%M", errors="coerce")
                # attach today's date to times
                today = pd.Timestamp.now().normalize()
                df["Date"] = [
                    today
                    + pd.Timedelta(hours=t.hour, minutes=t.minute, seconds=t.second)
                    for t in parsed
                ]
            else:
                # general parse (some may be full datetime strings)
                # try to parse quickly; if many fail, we'll fallback to trying common time-only formats
                df["Date"] = pd.to_datetime(
                    df["raw_time"], errors="coerce", dayfirst=False
                )

                if df["Date"].isna().all():
                    # fallback: try time-only formats explicitly
                    parsed = pd.to_datetime(strs, format="%H:%M:%S", errors="coerce")
                    if parsed.isna().all():
                        parsed = pd.to_datetime(strs, format="%H:%M", errors="coerce")
                    if parsed.isna().all():
                        # last resort: general parse (may warn), but do it once
                        df["Date"] = pd.to_datetime(df["raw_time"], errors="coerce")
                    else:
                        today = pd.Timestamp.now().normalize()
                        df["Date"] = [
                            today
                            + pd.Timedelta(
                                hours=t.hour, minutes=t.minute, seconds=t.second
                            )
                            for t in parsed
                        ]
                # else some rows parsed as real datetimes and we keep them

            # if still any NaT, fill forward/backward or create monotonic times
            if df["Date"].isna().any():
                # try forward fill, then backward fill, then create artificial increasing times
                df["Date"] = df["Date"].ffill().bfill()
                if df["Date"].isna().any():
                    base = pd.Timestamp.now().normalize()
                    df["Date"] = [
                        base + pd.Timedelta(seconds=i) for i in range(len(df))
                    ]

            df.set_index("Date", inplace=True)

            # require at least 2 points
            if len(df) < 2:
                print("Not enough points for candlestick. Need at least 2.")
                return list(zip(prices, times))

            # decide target number of candles (~20..150)
            desired_candles = min(150, max(20, len(df) // 2))
            total_seconds = (df.index[-1] - df.index[0]).total_seconds()

            if total_seconds <= 0:
                # identical timestamps -> group by fixed-size chunks
                n = max(1, math.ceil(len(df) / desired_candles))
                df["_grp"] = np.arange(len(df)) // n
                ohlc = df.groupby("_grp")["Close"].agg(
                    Open="first", High="max", Low="min", Close="last"
                )
                # take the first datetime of each group as the timestamp index (use groupby first on reset_index)
                group_first_times = df.reset_index().groupby("_grp")["Date"].first()
                ohlc.index = pd.DatetimeIndex(group_first_times.values)
            else:
                # dynamic resample rule based on desired candle count
                interval_seconds = max(1, int(total_seconds / desired_candles))
                if interval_seconds % 60 == 0:
                    rule = f"{interval_seconds // 60}T"
                else:
                    rule = f"{interval_seconds}S"
                ohlc = df["Close"].resample(rule).ohlc()
                ohlc.dropna(inplace=True)

            if ohlc.empty:
                print(
                    "After resampling/grouping there is no OHLC data — try reducing `total` or changing time parsing."
                )
                return list(zip(prices, times))

            # ensure DatetimeIndex
            if not isinstance(ohlc.index, pd.DatetimeIndex):
                try:
                    ohlc.index = pd.to_datetime(ohlc.index)
                except Exception as e:
                    print("Could not convert ohlc index to DatetimeIndex:", e)
                    return list(zip(prices, times))

            # draw with mplfinance
            mc = mpf.make_marketcolors(
                up="#00ff00", down="#ff4c4c", edge="inherit", wick="inherit"
            )
            style = mpf.make_mpf_style(marketcolors=mc, gridstyle="--", facecolor="0.1")
            title = f"{currency_name.capitalize()} Candlestick ({'Toman' if in_toman else 'Rial'})"

            if save:
                # filename می‌تواند 'euro_candle.png' یا 'euro_candle.svg' باشد
                save_opts = dict(
                    fname="euro_candle.png",
                    dpi=600,
                    bbox_inches="tight",
                    pad_inches=0.04,
                )

                mpf.plot(
                    ohlc,
                    type="candle",
                    style=style,
                    title=title,
                    ylabel="Price (Toman)",
                    volume=False,
                    savefig=save_opts,
                )

                print(f"Candlestick chart saved to {filename}")
            else:
                mpf.plot(
                    ohlc,
                    type="candle",
                    style=style,
                    title=title,
                    ylabel="Price (Toman)" if in_toman else "Price (Rial)",
                    volume=False,
                )

            return list(zip(prices, times))


