import pandas as pd
import requests
import os
import csv
import shutil
from datetime import datetime, time
import yfinance as yf
from ta.trend import EMAIndicator
from ta.momentum import StochasticOscillator
import math

class DataEngine:
    def __init__(self, symbol=None, url=None, provider=None, data_dir='data_cache'):
        # 1. Clean the incoming symbol
        self.symbol = symbol.strip().upper() if symbol else None
        
        # 2. Setup centralized paths
        base_path = os.path.dirname(os.path.abspath(__file__))
        self.cache_dir = os.path.join(base_path, data_dir)
        os.makedirs(self.cache_dir, exist_ok=True)

        # 3. Load the master instrument list to find URLs/Providers
        # This ensures the engine knows where to go for special tickers
        self.master_instruments = self.load_instruments_from_csv('instruments.csv')
        
        # 4. Find config from master list or use passed-in arguments
        instrument_config = next((i for i in self.master_instruments if i['symbol'] == self.symbol), None)
        
        if instrument_config:
            self.url = instrument_config['url']
            self.provider = instrument_config['provider']
        else:
            # Fallback to arguments if ticker isn't in the CSV list
            self.url = url
            self.provider = provider or 'yahoo'

        # 5. Define final file path for centralized storage
        if self.symbol:
            self.file_path = os.path.join(self.cache_dir, f"{self.symbol}.csv")
        else:
            self.file_path = None
        
        self.ensure_data()

    def ensure_data(self):
        """Checks if file exists; if not, downloads it."""
        if os.path.exists(self.file_path):
            return True # Data is already there
        
        print(f"DEBUG: {self.symbol} not found in cache. Attempting download...")
        
        try:
            # For a generic ticker like SPY, we use yfinance
            import yfinance as yf
            df = yf.download(self.symbol, period="max")
            
            if df.empty:
                print(f"ERROR: No data found for {self.symbol}")
                return False
                
            # Clean and save
            # 1. If columns are MultiIndex (tuples), take just the first level (the price name)
            if isinstance(df.columns, pd.MultiIndex):
                df.columns = df.columns.get_level_values(0)           
            # 2. Reset index to turn 'Date' into a column
            df.reset_index(inplace=True)
            # 3. Now it is safe to lowercase the column names
            df.columns = [str(c).lower() for c in df.columns]

            df.to_csv(self.file_path, index=False)
            print(f"DEBUG: Successfully cached {self.symbol}")
            return True
            
        except Exception as e:
            print(f"ERROR: Download failed for {self.symbol}: {e}")
            return False
    
    def load_instruments_from_csv(self, file_path):
        instruments = []
        
        TEMPLATES = {
            'jpm': "https://am.jpmorgan.com/FundsMarketingHandler/historicalData?cusip={cusip}&country=hk&role=per",
            'yahoo': "https://query1.finance.yahoo.com/v8/finance/chart/{cusip}?period1=0&period2=9999999999&interval=1d&events=history",
            'agi': "https://markets.ft.com/data/funds/tearsheet/historical?s={cusip}"
        }

        try:
            # Get absolute path relative to this script
            abs_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), file_path)
            
            if not os.path.exists(abs_path):
                print(f"Error: Master list {file_path} not found at {abs_path}")
                return []

            with open(abs_path, mode='r', encoding='utf-8-sig') as csvfile:
                reader = csv.DictReader(csvfile)
                # Clean header names (lowercase + remove whitespace)
                reader.fieldnames = [name.strip().lower() for name in reader.fieldnames]
                
                for row in reader:
                    # Use .get() with fallback to avoid KeyErrors
                    symbol = (row.get('symbol') or '').strip().upper()
                    cusip = (row.get('cusip') or '').strip()
                    provider = (row.get('provider') or 'jpm').strip().lower()
                    
                    if symbol and cusip:
                        template = TEMPLATES.get(provider, TEMPLATES['jpm'])
                        url = template.format(cusip=cusip)
                        
                        instruments.append({
                            "symbol": symbol, 
                            "url": url, 
                            "provider": provider,
                            "cusip": cusip
                        })
                        
        except Exception as e:
            print(f"CRITICAL: Failed to load instruments.csv: {e}")
                    
        return instruments

    def _ensure_data_exists(self):
        if not os.path.exists(self.file_path):
            # Check if this symbol exists in our master CSV mapping
            match = next((i for i in self.instruments if i['symbol'].upper() == self.symbol), None)
            
            if match:
                print(f"DEBUG: Found {self.symbol} in master list. Fetching from {match['provider']}...")
                self._download_from_provider(match)
            else:
                print(f"DEBUG: {self.symbol} not in master list. Trying generic Yahoo Finance...")
                self._download_generic_yahoo()
    
    def _download_generic_yahoo(self):
        """Standard yfinance fallback"""
        try:
            df = yf.download(self.symbol, period="max")
            if not df.empty:
                df.reset_index(inplace=True)
                df.columns = [c.lower() for c in df.columns]
                df.to_csv(self.file_path, index=False)
        except Exception as e:
            print(f"Yahoo fallback failed: {e}")
            
    def global_sync(self):
        """Backup, Sync all instruments, and return a summary report."""
        # 1. Run Maintenance/Backup
        self.run_pre_sync_maintenance()
        
        # FIX 1: Add 'self.' so it calls the method inside this class
        instruments = self.load_instruments_from_csv('instruments.csv')
        
        report = {
            "total": len(instruments),
            "updated": 0,
            "failed": 0,
            "details": []
        }
        
        for item in instruments:
            try:
                self.symbol = item['symbol']
                self.provider = item['provider']
                self.url = item['url']
                
                # FIX 2: Use 'self.cache_dir' to match your __init__ logic
                self.file_path = os.path.join(self.cache_dir, f"{self.symbol}.csv")
                
                print(f"Updating {self.symbol}...")
                
                # fetch_data now returns the updated DataFrame or None
                result_df = self.fetch_data()

                time.sleep(1)
                
                if result_df is not None and not result_df.empty:
                    report["updated"] += 1
                    last_price = result_df['close'].iloc[-1]
                    report["details"].append(f"✅ {self.symbol}: Updated (Price: {last_price})")
                else:
                    report["failed"] += 1
                    report["details"].append(f"❌ {self.symbol}: No new data found")
                    
            except Exception as e:
                report["failed"] += 1
                report["details"].append(f"⚠️ {self.symbol}: Error ({str(e)})")
                
        return report
    
    def run_pre_sync_maintenance(self):
        """Backs up files and reports current data health."""
        import os
        import shutil
        import pandas as pd
        from datetime import datetime

        # 1. Setup paths correctly
        base_dir = os.path.dirname(os.path.abspath(__file__))
        backup_dir = os.path.join(base_dir, 'backups')
        
        # 2. Create the timestamped folder path FIRST
        timestamp = datetime.now().strftime("%Y%m%d_%H%M")
        current_backup_path = os.path.join(backup_dir, f"sync_backup_{timestamp}")
        
        # 3. Create the directories (safety-first)
        os.makedirs(current_backup_path, exist_ok=True)
        
        print(f"\n--- Pre-Sync Health Check ({timestamp}) ---")
        stats = []

        # 4. Check if cache exists to avoid errors
        if not os.path.exists(self.cache_dir):
            print(f"⚠️ Cache directory not found at {self.cache_dir}")
            return pd.DataFrame()

        # 5. Backup loop
        for filename in os.listdir(self.cache_dir): 
            if filename.endswith(".csv"):
                src = os.path.join(self.cache_dir, filename)
                dst = os.path.join(current_backup_path, filename)
                
                try:
                    # Perform copy
                    shutil.copy2(src, dst)
                    
                    # Read data for health check
                    df = pd.read_csv(src)
                    
                    # Store stats
                    stats.append({
                        "Fund": filename.replace(".csv", ""),
                        "Rows": len(df),
                        "Start": df['date'].min() if 'date' in df.columns else "N/A",
                        "End": df['date'].max() if 'date' in df.columns else "N/A"
                    })
                    print(f"📦 Backed up: {filename} ({len(df)} rows)")
                    
                except Exception as e:
                    print(f"⚠️ Could not backup {filename}: {e}")
                    continue

        # 6. Display and return report
        if stats:
            stats_df = pd.DataFrame(stats)
            print("\n" + stats_df.to_string(index=False))
            print(f"\n✅ All backups saved to: {current_backup_path}")
            return stats_df
        else:
            print("📭 No CSV files found to backup.")
            return pd.DataFrame()

    def _parse_jpm(self, json_data):
        if isinstance(json_data, dict) and "historicalNAVList" in json_data:
            df = pd.DataFrame(json_data["historicalNAVList"])
            return df.rename(columns={'navPrice': 'close', 'date': 'date'})
        return None

    def _parse_ft_html(self, html_text):
        try:
            # 1. Use BeautifulSoup to handle the nested spans in the Date column
            from bs4 import BeautifulSoup
            soup = BeautifulSoup(html_text, 'html.parser')
            
            # Find the specific results table
            table = soup.find('table', class_='mod-tearsheet-historical-prices__results')
            if not table:
                print(f"❌ Could not find the results table in the HTML for {self.symbol}")
                return None

            data = []
            rows = table.find('tbody').find_all('tr')
            
            for row in rows:
                cols = row.find_all('td')
                if len(cols) >= 5:
                    # The Date cell has two spans. We'll take the first one (Full date).
                    date_cell = cols[0].find('span', class_='mod-ui-hide-small-below')
                    date_str = date_cell.get_text(strip=True) if date_cell else cols[0].get_text(strip=True)
                    
                    # The Close price is usually the 5th column (index 4)
                    close_str = cols[4].get_text(strip=True).replace(',', '')
                    
                    data.append({
                        'date': date_str,
                        'close': close_str
                    })

            # 2. Convert to DataFrame
            df = pd.DataFrame(data)
            if df.empty:
                return None

            # 3. Final Type Conversion
            df['date'] = pd.to_datetime(df['date'], errors='coerce')
            df['close'] = pd.to_numeric(df['close'], errors='coerce')
            
            return df.dropna().sort_values('date').reset_index(drop=True)

        except Exception as e:
            print(f"❌ Failed to parse FT HTML structure: {e}")
            return None
    
    def _parse_yahoo(self, json_data):
        """Parses Yahoo Finance v8 Chart JSON"""
        try:
            chart = json_data['chart']['result'][0]
            timestamps = chart['timestamp']
            indicators = chart['indicators']['quote'][0]
            # Use adjclose if available, otherwise close
            closes = indicators.get('close', [])
            df = pd.DataFrame({
                'date': pd.to_datetime(timestamps, unit='s'),
                'close': closes
            })
            return df
        except:
            return None
    
    def fetch_data(self):
        local_df = pd.DataFrame()
        new_df = None

        # 1. Load Local Cache & Force Date Type
        if os.path.exists(self.file_path):
            try:
                local_df = pd.read_csv(self.file_path)
                local_df = local_df.loc[:, ~local_df.columns.duplicated()].copy()
                local_df.columns = [c.lower().strip() for c in local_df.columns]
                local_df = local_df.rename(columns={'price': 'close', 'nav': 'close'})
                
                # FORCE CONVERSION: This fixes the '<' error
                # errors='coerce' turns bad text into NaT (Not a Time), which we then drop
                local_df['date'] = pd.to_datetime(local_df['date'], errors='coerce')
                local_df = local_df.dropna(subset=['date']).reset_index(drop=True)
            except Exception as e:
                print(f"Local Load Error: {e}")

        # 2. Network Fetch
        try:
            headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
            response = requests.get(self.url, headers=headers, timeout=15)
            response.raise_for_status()

            if self.provider == 'agi':
                new_df = self._parse_ft_html(response.text)
            elif self.provider == 'jpm':
                new_df = self._parse_jpm(response.json())
            elif self.provider == 'yahoo':
                new_df = self._parse_yahoo(response.json())

            # 3. Safe Merge & Sort
            if new_df is not None and not new_df.empty:
                # Force new_df dates to match local_df format
                new_df['date'] = pd.to_datetime(new_df['date'], errors='coerce')
                
                combined_df = pd.concat([local_df, new_df], ignore_index=True)
                combined_df = combined_df.drop_duplicates(subset=['date'], keep='last')
                
                # SORTING: Now safe because all types are Timestamps
                combined_df = combined_df.sort_values('date').reset_index(drop=True)
                
                if 'close' in combined_df.columns:
                    final_df = combined_df[['date', 'close']].dropna()
                    final_df.to_csv(self.file_path, index=False)
                    return final_df
            
            return local_df

        except Exception as e:
            print(f"Network error for {self.symbol}: {e}")
            return local_df
    
    def get_local_metrics(self):
        """Reads ONLY from local CSV and returns metrics immediately."""
        if not os.path.exists(self.file_path):
            return {"error": "Missing Local Data", "status": "needs_sync"}
        
        try:
            df = pd.read_csv(self.file_path)
            # Ensure columns are clean
            df.columns = [c.lower().strip() for c in df.columns]
            df['date'] = pd.to_datetime(df['date'], errors='coerce')
            df = df.dropna(subset=['date', 'close']).sort_values('date')
            
            # Pass this local dataframe to your existing calculation function
            return self.calculate_table_metrics(df)
        except Exception as e:
            print(f"Error reading local data for {self.symbol}: {e}")
            return None
    
        
    def calculate_table_metrics(self, df):
        if df is None or df.empty or len(df) < 2:
            return None
        
        last_close = float(df.iloc[-1]['close'])
        prev_close = float(df.iloc[-2]['close'])
        change_pct = ((last_close - prev_close) / prev_close) * 100
        count = len(df)

        def get_ema_offset(window):
            if count >= window:
                ema = EMAIndicator(close=df['close'], window=window).ema_indicator().iloc[-1]
                return round(((last_close / ema) * 100) - 100, 1)
            return "N/A"

        k_val = d_val = "N/A"
        if count >= 14:
            high_14 = df['close'].rolling(window=14).max()
            low_14 = df['close'].rolling(window=14).min()
            stoch = StochasticOscillator(high=high_14, low=low_14, close=df['close'], window=14)
            k_val = round(stoch.stoch().iloc[-1], 0)
            d_val = round(stoch.stoch_signal().iloc[-1], 0)

        return {
            "last_close": round(last_close, 2),
            "change_pct": round(change_pct, 2),
            "low_52": round(float(df.tail(252)['close'].min()), 2),
            "high_52": round(float(df.tail(252)['close'].max()), 2),
            "last_ema20": get_ema_offset(20),
            "last_ema50": get_ema_offset(50),
            "last_ema100": get_ema_offset(100),
            "last_ema200": get_ema_offset(200),
            "kd_values": f"{k_val}/{d_val}" if k_val != "N/A" else "N/A"
        }

class StrategyEngine:
    """
    Handles financial strategy simulations and backtesting.
    This class takes a DataEngine instance to access files.
    """
    def __init__(self, data_engine):
        # 1. Save the engine object (The 'Supplier')
        self.data_engine = data_engine
        
        # 2. Extract the symbol from the supplier so the chef knows the name
        # We don't need .strip() here because DataEngine already did it!
        self.symbol = data_engine.symbol

    def _find_file(self):
        # Try the uppercase version first
        upper_path = os.path.join(self.data_dir, f"{self.symbol}.csv")
        # Try the lowercase version second
        lower_path = os.path.join(self.data_dir, f"{self.symbol.lower()}.csv")
        
        if os.path.exists(upper_path):
            return upper_path
        elif os.path.exists(lower_path):
            return lower_path
        
        # If neither exists, print a very specific message to your terminal
        print(f"ERROR: Searched for {upper_path} AND {lower_path} - Neither found!")
        return None

    def load_data(self):
        df = pd.read_csv(self.file_path)
        # Standardize column names to lowercase to avoid 'Price' vs 'price' issues
        df.columns = [c.lower() for c in df.columns]
        
        # Map common variations to a single 'price' column
        if 'adj close' in df.columns:
            df = df.rename(columns={'adj close': 'close'})
        elif 'close' in df.columns:
            df = df.rename(columns={'close': 'close'})
            
        return df

    def calculate_va_vs_dca(self, initial_inv, monthly_target, start_date, allow_sell=True, allow_fractional=True):
        import math
        
        # 1. Load and Prepare Data
        df = pd.read_csv(self.data_engine.file_path)
        df['date'] = pd.to_datetime(df['date'])
        df = df.sort_values('date')

        # 2. Identify the "Anchor Day" and the "Absolute Latest Day"
        start_dt_obj = pd.to_datetime(start_date)
        anchor_day = start_dt_obj.day 
        latest_csv_date = df['date'].max() # This captures 2026-01-27

        # --- 3. Filter data starting from your start_date ---
        df_from_start = df[df['date'] >= start_dt_obj].copy()
        if df_from_start.empty:
            return []

        # --- 4. Choose rows based on Frequency ---
        if frequency == "Weekly":
            # Take every 5th trading day
            final_df = df_from_start.iloc[::5].copy()
            step_increment = float((monthly_goal * 12) / 52)

        elif frequency == "Bi-Weekly":
            # Take every 10th trading day
            final_df = df_from_start.iloc[::10].copy()
            step_increment = float((monthly_goal * 12) / 26)

        else: # Default to Monthly
            # Group by year/month and take the first available day >= anchor_day
            final_df = df_from_start[df_from_start['date'].dt.day >= anchor_day].groupby([
                df_from_start['date'].dt.year, 
                df_from_start['date'].dt.month
            ], as_index=False).first()
            step_increment = float(monthly_goal)

        # --- 5. FORCE LAST ROW SAFETY ---
        # Reset index to make concatenation clean
        final_df = final_df.reset_index(drop=True)
        
        if not df_from_start.empty:
            latest_row = df_from_start.iloc[[-1]] # Gets the absolute last available day
            if final_df.empty or latest_row.iloc[0]['date'] != final_df.iloc[-1]['date']:
                final_df = pd.concat([final_df, latest_row]).drop_duplicates(subset=['date'])
        
        # Sort and final index reset before the loop
        final_df = final_df.sort_values('date').reset_index(drop=True)

        if final_df.empty:
            return []
        
        # --- 6. Helper for share calculation ---
        def get_shares(cash, prc):
            if prc <= 0: return 0
            # Note: allow_fractional should be defined in your class/scope
            return cash / prc if allow_fractional else math.floor(cash / prc)

        # --- 7. Initial Setup ---
        va_shares = 0
        dca_shares = 0
        va_invested = 0
        dca_invested = 0
        va_target_value = 0
        history = []

        # --- 8. Strategy Loop ---
        for step, (idx, row) in enumerate(final_df.iterrows()):
            actual_date_str = row['date'].strftime('%Y-%m-%d')
            price = float(row['close'])
            
            if step == 0:
                # --- STEP 0: INITIAL DEPOSIT ---
                actual_inv = initial_inv 
                dca_actual_inv = initial_inv
                
                va_target_value = initial_inv
                va_new_shares = get_shares(actual_inv, price)
                dca_new_shares = va_new_shares
                
            else:
                # --- STEP 1+: DVA vs DCA ---
                # Use step_increment (calculated in your frequency logic) 
                # to ensure growth matches frequency (Weekly/Monthly)
                current_increment = step_increment 

                # DCA Logic
                dca_actual_inv = current_increment
                dca_new_shares = get_shares(dca_actual_inv, price)

                # DVA Logic (Fixed Value Path)
                va_target_value += current_increment
                
                # Gap calculation: Target vs. current value BEFORE this step's investment
                current_va_val_pre = va_shares * price
                diff = va_target_value - current_va_val_pre
                
                # Apply Buy/Sell constraints
                # note: allow_sell should be defined in your class/scope
                actual_inv = diff if (diff >= 0 or allow_sell) else 0
                va_new_shares = get_shares(actual_inv, price)

            # --- STATE UPDATES ---
            va_shares += va_new_shares
            dca_shares += dca_new_shares
            
            va_invested += actual_inv
            dca_invested += dca_actual_inv

            # --- Unified History Append ---
            history.append({
                "date": actual_date_str,
                "price": round(price, 2),
                "dca_value": round(dca_shares * price, 2),
                "dca_invested": round(dca_invested, 2),
                "dca_shares_trans": round(dca_new_shares, 4),
                "dca_shares_total": round(dca_shares, 4),
                "va_value": round(va_shares * price, 2),
                "va_invested": round(va_invested, 2),
                "va_diff": round(actual_inv, 2),
                "va_shares_trans": round(va_new_shares, 4),
                "va_shares_total": round(va_shares, 4),
                "va_target_value": round(va_target_value, 2)
            })
            
        return history   
    
    def run_simulation(self, start_date, monthly_goal, initial_inv, frequency="Monthly", allow_sell=True, allow_fractional=True):
        # 1. DATA LOADING
        df = pd.read_csv(self.data_engine.file_path)
        df['date'] = pd.to_datetime(df['date'])
        df = df.sort_values('date')

        # 2. DATE PREP
        start_dt_obj = pd.to_datetime(start_date)
        latest_csv_date = df['date'].max()
        
        # Filter data starting from user's choice
        df_from_start = df[df['date'] >= start_dt_obj].copy()
        
        if df_from_start.empty:
            return []

        # 3. FREQUENCY LOGIC (Hardened)
        # Standardize string for comparison
        freq_check = str(frequency).strip().title() 

        if freq_check == "Weekly":
            final_df = df_from_start.iloc[::5].copy()
            step_increment = float((monthly_goal * 12) / 52)
        elif freq_check == "Bi-Weekly":
            final_df = df_from_start.iloc[::10].copy()
            step_increment = float((monthly_goal * 12) / 26)
        else:
            # For Monthly, we need the anchor day
            anchor_day = start_dt_obj.day
            final_df = df_from_start[df_from_start['date'].dt.day >= anchor_day].groupby([
                df_from_start['date'].dt.year, 
                df_from_start['date'].dt.month
            ], as_index=False).first()
            step_increment = float(monthly_goal)

        # 4. SAFETY: Ensure most recent price is included
        final_df = final_df.reset_index(drop=True)
        last_actual_row = df_from_start.iloc[[-1]]
        
        if final_df.empty or last_actual_row.iloc[0]['date'] != final_df.iloc[-1]['date']:
            final_df = pd.concat([final_df, last_actual_row]).drop_duplicates(subset=['date'])

        final_df = final_df.sort_values('date').reset_index(drop=True)

        # 5. STRATEGY INITIALIZATION
        def get_shares(cash, prc):
            if prc <= 0: return 0
            if allow_fractional:
                return float(cash / prc)
            else:
                # Handles both buying (+) and selling (-) for whole shares
                return float(math.floor(cash / prc)) if cash >= 0 else float(math.ceil(cash / prc))

        # Ensure these are initialized as floats
        va_shares, dca_shares = 0.0, 0.0
        va_invested, dca_invested = 0.0, 0.0
        
        history = []

        for step, (idx, row) in enumerate(final_df.iterrows()):
            actual_date_str = row['date'].strftime('%Y-%m-%d')
            price = float(row['close'])
            
            if step == 0:
                # First row: Both strategies start with the Initial Investment
                va_actual_inv = float(initial_inv)
                dca_actual_inv = float(initial_inv)
                va_target_value = float(initial_inv)
            else:
                # Subsequent rows: Use the frequency-adjusted step_increment
                va_target_value += step_increment
                
                # DCA logic: Always invests the same amount every period
                dca_actual_inv = float(step_increment)
                
                # VA logic: Invests enough to hit the target value
                current_va_market_val = va_shares * price
                diff = va_target_value - current_va_market_val
                
                # Apply "Allow Sell" constraint
                va_actual_inv = diff if (diff >= 0 or allow_sell) else 0.0

            # Update Shares based on fractional setting
            va_new_shares = get_shares(va_actual_inv, price)
            dca_new_shares = get_shares(dca_actual_inv, price)

            # Running totals for shares
            va_shares += va_new_shares
            dca_shares += dca_new_shares
            
            # Running totals for principal invested
            va_invested += va_actual_inv
            dca_invested += dca_actual_inv

            history.append({
                "date": actual_date_str,
                "price": round(price, 2),
                "va_diff": round(va_actual_inv, 2),        # Invested this step (VA)
                "va_shares_trans": round(va_new_shares, 4),
                "va_value": round(va_shares * price, 2),   # Current Portfolio Value (VA)
                "va_invested": round(va_invested, 2),      # Total Out-of-Pocket (VA)
                "va_shares_total": round(va_shares, 4),
                "va_target_value": round(float(va_target_value), 2),
                "dca_diff": round(dca_actual_inv, 2),      # Invested this step (DCA)
                "dca_shares_trans": round(dca_new_shares, 4),
                "dca_value": round(dca_shares * price, 2), # Current Portfolio Value (DCA)
                "dca_invested": round(dca_invested, 2),    # Total Out-of-Pocket (DCA)
                "dca_shares_total": round(dca_shares, 4)
            })

        return history