import logging
import sys
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
import pytz
import pandas as pd
import numpy as np
from firstock import firstock
import os
import glob
import time
import json
from pathlib import Path

LOCK_FILE = "/tmp/puthayal.lock"

def acquire_lock(timeout=30, interval=3):
    start_time = time.time()
    print(f"[INFO] Attempting to acquire lock: {LOCK_FILE}")
    while True:
        try:
            fd = os.open(LOCK_FILE, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
            os.close(fd)
            print(f"[INFO] Lock acquired successfully: {LOCK_FILE}")
            return True
        except FileExistsError:
            if time.time() - start_time >= timeout:
                print("[ERROR] Timeout reached. Could not acquire lock.")
                return False
            time.sleep(interval)

def release_lock():
    try:
        os.remove(LOCK_FILE)
        print(f"[INFO] Lock file removed: {LOCK_FILE}")
    except FileNotFoundError:
        pass

def _get_config_path() -> Path:
    return Path(__file__).parent / 'symbol_config.json'

def _load_symbol_config() -> dict:
    config_path = _get_config_path()
    try:
        if config_path.exists():
            with open(config_path, 'r') as f:
                config = json.load(f)
                return config.get('symbol_mapping', {})
        return {}
    except Exception as e:
        print(f"[ERROR] Error loading config: {e}")
        return {}

class StockDataFetcher:
    def __init__(self, client_details: List[str]):
        self.client_details = client_details
        self.user_id = client_details[0]
        self.ist = pytz.timezone('Asia/Kolkata')
        self.logger = self.setup_logger()
        self.SYMBOL_MAPPING = _load_symbol_config()
        
        self.candle_count = 1000
        self.max_output_files = 5
        self.atr_length = 55
        
        self.output_data = {
            'timestamp': None,
            'symbol': 'NIFTY',
            'timeframes': {
                '5min': {
                    'historical_data': [],
                    'zones': [],
                    'support_resistance': {
                        'support': None,
                        'resistance': None
                    }
                },
                '1min': {
                    'historical_data': [],
                    'zones': [],
                    'support_resistance': {
                        'support': None,
                        'resistance': None
                    }
                }
            }
        }

    class ISTFormatter(logging.Formatter):
        def formatTime(self, record, datefmt=None):
            ist = pytz.timezone('Asia/Kolkata')
            record_time = datetime.fromtimestamp(record.created, tz=ist)
            return record_time.strftime(datefmt or '%Y-%m-%d %H:%M:%S')

    def setup_logger(self) -> logging.Logger:
        logger = logging.getLogger(__name__)
        logger.setLevel(logging.DEBUG)
        if not logger.handlers:
            formatter = self.ISTFormatter('%(asctime)s - %(levelname)s - %(message)s')
            console_handler = logging.StreamHandler()
            console_handler.setFormatter(formatter)
            logger.addHandler(console_handler)
        return logger

    def log_info(self, text):
        self.logger.info(text)

    def log_debug(self, text):
        self.logger.debug(text)

    def convert_to_serializable(self, obj):
        if isinstance(obj, (np.integer, np.int64)):
            return int(obj)
        elif isinstance(obj, (np.floating, np.float64)):
            return float(obj)
        elif isinstance(obj, np.ndarray):
            return obj.tolist()
        elif isinstance(obj, pd.Series):
            return obj.tolist()
        elif isinstance(obj, pd.DataFrame):
            return obj.to_dict('records')
        elif isinstance(obj, (pd.Timestamp, datetime)):
            return obj.isoformat()
        elif isinstance(obj, dict):
            return {k: self.convert_to_serializable(v) for k, v in obj.items()}
        elif isinstance(obj, list):
            return [self.convert_to_serializable(item) for item in obj]
        else:
            return obj

    def cleanup_old_files(self, output_dir: str, symbol: str):
        try:
            pattern = os.path.join(output_dir, f'{symbol}_*.json')
            files = glob.glob(pattern)
            symbol_files = [f for f in files if os.path.basename(f) != f'{symbol}_latest.json']
            symbol_files.sort(key=os.path.getmtime, reverse=True)
            for file_path in symbol_files[self.max_output_files:]:
                try:
                    os.remove(file_path)
                    self.log_info(f"  Deleted: {os.path.basename(file_path)}")
                except Exception as e:
                    self.log_debug(f"  Error deleting {file_path}: {e}")
        except Exception as e:
            self.log_debug(f"Error during cleanup: {e}")

    def login(self):
        try:
            self.logger.info(f"Attempting login for {self.client_details[0]}")
            response = firstock.login(*self.client_details)
            if response.get("status") == "success":
                self.logger.info("Login successful")
            else:
                self.logger.error(f"Login failed: {response}")
                sys.exit()
        except Exception as e:
            self.logger.error(f"Login error: {e}")
            sys.exit()

    def translate_symbol(self, symbol: str) -> str:
        symbol_lower = symbol.lower()
        if symbol_lower in self.SYMBOL_MAPPING:
            return self.SYMBOL_MAPPING[symbol_lower]
        else:
            self.logger.warning(f"Symbol '{symbol_lower}' not found, using default")
            return 'NSE:NIFTY'

    def fetch_time_price_series(self, exchange: str, trading_symbol: str, start_time: str, end_time: str, interval: str) -> pd.DataFrame:
        try:
            self.log_info(f"API Request: {exchange}:{trading_symbol}, Interval: {interval}")
            response = firstock.timePriceSeries(
                userId=self.user_id,
                exchange=exchange,
                tradingSymbol=trading_symbol,
                startTime=start_time,
                endTime=end_time,
                interval=interval
            )
            if response.get("status") == "success":
                data = response.get("data", [])
                if data:
                    df_temp = pd.DataFrame(data)
                    if 'time' in df_temp.columns:
                        df_temp['datetime'] = pd.to_datetime(df_temp['time'], format='%H:%M:%S %d-%m-%Y')
                    return df_temp
            return pd.DataFrame()
        except Exception as e:
            self.log_debug(f"Error fetching series: {e}")
            return pd.DataFrame()

    def fetch_candles(self, symbol: str, interval_minutes: int, count: int) -> pd.DataFrame:
        now_ist = datetime.now(self.ist)
        
        end_time = now_ist
        if now_ist.hour >= 15 and now_ist.minute >= 31:
            end_time = now_ist.replace(hour=15, minute=30, second=0)
        
        candles_per_day = 375 // interval_minutes
        required_days = (count // candles_per_day) + 5
        
        start_time = end_time - timedelta(days=required_days)
        
        if start_time.hour < 9 or (start_time.hour == 9 and start_time.minute < 15):
            start_time = start_time.replace(hour=9, minute=15, second=0)
        
        interval_str = f"{interval_minutes}mi"
        start_str = start_time.strftime("%H:%M:%S %d-%m-%Y")
        end_str = end_time.strftime("%H:%M:%S %d-%m-%Y")
        
        translated_symbol = self.translate_symbol(symbol)
        exchange, trading_symbol = translated_symbol.split(":")
        
        self.log_info(f"Fetching {interval_minutes}min data (target: {count} candles)")
        self.log_info(f"  Start: {start_str}")
        self.log_info(f"  End: {end_str}")
        
        df = self.fetch_time_price_series(exchange, trading_symbol, start_str, end_str, interval_str)
        
        if df.empty:
            return df
        
        df = df.sort_values(by='datetime', ascending=True)
        
        # Filter to trading hours
        df = df[(df['datetime'].dt.hour >= 9) & (df['datetime'].dt.hour <= 15)]
        df = df[(df['datetime'].dt.hour != 9) | (df['datetime'].dt.minute >= 15)]
        
        if len(df) > count:
            df = df.tail(count)
            self.log_info(f"  Trimmed to last {count} candles")
        else:
            self.log_info(f"  Only {len(df)} candles available (requested {count})")
        
        self.log_info(f"  Retrieved {len(df)} candles")
        return df

    def calculate_atr(self, df: pd.DataFrame, length: int = 55) -> pd.Series:
        """Calculate ATR for the given DataFrame"""
        high = df['high']
        low = df['low']
        close = df['close']
        
        tr1 = high - low
        tr2 = abs(high - close.shift())
        tr3 = abs(low - close.shift())
        tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
        atr = tr.rolling(window=length).mean()
        return atr

    def detect_order_blocks(self, df: pd.DataFrame) -> Dict[str, Any]:
        """
        Detect Order Blocks - SIMPLIFIED but effective approach
        """
        df = df.copy()
        n = len(df)
        
        self.log_info(f"Starting Order Block detection on {n} candles")
        
        # ============================================================
        # 1. Find ALL swing highs and lows (using 2-bar lookback)
        # ============================================================
        swing_highs = []
        swing_lows = []
        
        for i in range(2, n-2):
            # Swing high: high[i] > high[i-1] and high[i] > high[i+1]
            if (df['high'].iloc[i] > df['high'].iloc[i-1] and 
                df['high'].iloc[i] > df['high'].iloc[i+1]):
                swing_highs.append(i)
            # Swing low: low[i] < low[i-1] and low[i] < low[i+1]
            if (df['low'].iloc[i] < df['low'].iloc[i-1] and 
                df['low'].iloc[i] < df['low'].iloc[i+1]):
                swing_lows.append(i)
        
        self.log_info(f"Found {len(swing_highs)} swing highs and {len(swing_lows)} swing lows")
        
        # ============================================================
        # 2. Find MAJOR levels (swings that get broken)
        # ============================================================
        major_highs = []  # (bar_index, price)
        major_lows = []   # (bar_index, price)
        
        # For each swing high, check if price breaks above it later
        for idx in swing_highs:
            price = df['high'].iloc[idx]
            # Look ahead up to 30 bars
            for j in range(idx + 3, min(idx + 30, n)):
                if df['close'].iloc[j] > price:
                    # This swing high was broken - it's a major level
                    if j - idx >= 5:  # At least 5 bars before break
                        major_highs.append((idx, price))
                    break
        
        # For each swing low, check if price breaks below it later
        for idx in swing_lows:
            price = df['low'].iloc[idx]
            for j in range(idx + 3, min(idx + 30, n)):
                if df['close'].iloc[j] < price:
                    if j - idx >= 5:
                        major_lows.append((idx, price))
                    break
        
        self.log_info(f"Found {len(major_highs)} major highs and {len(major_lows)} major lows")
        
        # ============================================================
        # 3. Create zones at major levels
        # ============================================================
        zones = []
        created_zones = set()
        
        # Demand zones from major lows (price broke above them)
        for idx, price in major_lows:
            if idx not in created_zones:
                created_zones.add(idx)
                zone = {
                    'type': 'demand',
                    'start_bar': int(idx),
                    'end_bar': int(idx + 5),  # Will be extended
                    'proximal': float(df['high'].iloc[idx]),
                    'distal': float(df['low'].iloc[idx]),
                    'label': 'D',
                    'active': True
                }
                zones.append(zone)
                self.log_info(f"🟢 Demand Zone at bar {idx} | High: {df['high'].iloc[idx]:.2f}, Low: {df['low'].iloc[idx]:.2f}")
        
        # Supply zones from major highs (price broke below them)
        for idx, price in major_highs:
            if idx not in created_zones:
                created_zones.add(idx)
                zone = {
                    'type': 'supply',
                    'start_bar': int(idx),
                    'end_bar': int(idx + 5),
                    'proximal': float(df['low'].iloc[idx]),
                    'distal': float(df['high'].iloc[idx]),
                    'label': 'S',
                    'active': True
                }
                zones.append(zone)
                self.log_info(f"🔴 Supply Zone at bar {idx} | High: {df['high'].iloc[idx]:.2f}, Low: {df['low'].iloc[idx]:.2f}")
        
        # ============================================================
        # 4. Dynamic Zone Extension
        # ============================================================
        for i in range(50, n):
            for zone in zones:
                if zone.get('active', False):
                    # Skip if we haven't reached the start bar yet
                    if i <= zone['start_bar']:
                        zone['end_bar'] = i
                        continue
                    
                    if zone['type'] == 'demand':
                        # Demand zone invalidated when price breaks below distal
                        if df['low'].iloc[i] < zone['distal']:
                            zone['active'] = False
                            zone['end_bar'] = i
                        else:
                            zone['end_bar'] = i
                    else:  # supply
                        # Supply zone invalidated when price breaks above distal
                        if df['high'].iloc[i] > zone['distal']:
                            zone['active'] = False
                            zone['end_bar'] = i
                        else:
                            zone['end_bar'] = i
        
        # ============================================================
        # 5. Filter to most recent zones
        # ============================================================
        active_demand = [z for z in zones if z.get('active', False) and z['type'] == 'demand']
        inactive_demand = [z for z in zones if not z.get('active', True) and z['type'] == 'demand'][-5:]
        active_supply = [z for z in zones if z.get('active', False) and z['type'] == 'supply']
        inactive_supply = [z for z in zones if not z.get('active', True) and z['type'] == 'supply'][-5:]
        
        final_zones = active_demand + inactive_demand + active_supply + inactive_supply
        
        for zone in final_zones:
            if 'end_bar' not in zone:
                zone['end_bar'] = zone['start_bar'] + 1
        
        final_zones.sort(key=lambda x: x['start_bar'])
        
        # Get support/resistance from latest levels
        support = major_lows[-1][1] if major_lows else None
        resistance = major_highs[-1][1] if major_highs else None
        
        support_str = f"{support:.2f}" if support else 'None'
        resistance_str = f"{resistance:.2f}" if resistance else 'None'
        
        self.log_info(f"\n{'='*60}")
        self.log_info("DETECTION SUMMARY")
        self.log_info(f"{'='*60}")
        self.log_info(f"Total candles: {n}")
        self.log_info(f"Zones created: {len(zones)} (Demand: {len([z for z in zones if z['type']=='demand'])}, Supply: {len([z for z in zones if z['type']=='supply'])})")
        self.log_info(f"Final zones: {len(final_zones)} (Active D: {len(active_demand)}, Active S: {len(active_supply)})")
        self.log_info(f"Support: {support_str}, Resistance: {resistance_str}")
        self.log_info(f"{'='*60}\n")
        
        return {
            'zones': final_zones,
            'support_resistance': {
                'support': float(support) if support else None,
                'support_bar': major_lows[-1][0] if major_lows else 0,
                'resistance': float(resistance) if resistance else None,
                'resistance_bar': major_highs[-1][0] if major_highs else 0
            }
        }

    def prepare_timeframe_data(self, df: pd.DataFrame) -> Dict[str, Any]:
        if df.empty:
            return {
                'historical_data': [],
                'zones': [],
                'support_resistance': {'support': None, 'resistance': None}
            }
        
        result = self.detect_order_blocks(df)
        
        historical_data = []
        for idx, row in df.iterrows():
            historical_data.append({
                'datetime': row['datetime'].isoformat(),
                'open': float(round(row['open'], 2)),
                'high': float(round(row['high'], 2)),
                'low': float(round(row['low'], 2)),
                'close': float(round(row['close'], 2))
            })
        
        return {
            'historical_data': historical_data,
            'zones': result['zones'],
            'support_resistance': result['support_resistance']
        }

    def fetch_all_data(self, symbol: str = 'nifty'):
        display_symbol = symbol.upper()
        self.output_data['symbol'] = display_symbol
        self.output_data['timestamp'] = datetime.now(self.ist).isoformat()
        
        self.log_info(f"\n{'='*60}")
        self.log_info(f"Fetching {display_symbol} data")
        self.log_info(f"{'='*60}")
        
        # Fetch 5-min data
        self.log_info(f"\n--- 5-MIN TIMEFRAME ---")
        df_5min = self.fetch_candles(symbol, 5, self.candle_count)
        if not df_5min.empty:
            self.log_info(f"✅ 5-min data: {len(df_5min)} candles")
            self.output_data['timeframes']['5min'] = self.prepare_timeframe_data(df_5min)
        else:
            self.log_info("❌ Failed to fetch 5-min data")
        
        # Fetch 1-min data
        self.log_info(f"\n--- 1-MIN TIMEFRAME ---")
        df_1min = self.fetch_candles(symbol, 1, self.candle_count)
        if not df_1min.empty:
            self.log_info(f"✅ 1-min data: {len(df_1min)} candles")
            self.output_data['timeframes']['1min'] = self.prepare_timeframe_data(df_1min)
        else:
            self.log_info("❌ Failed to fetch 1-min data")
        
        # Print summary
        self.log_info(f"\n{'='*60}")
        self.log_info("DATA FETCH SUMMARY")
        self.log_info(f"{'='*60}")
        for tf in ['5min', '1min']:
            data = self.output_data['timeframes'][tf]
            zones = data.get('zones', [])
            active_zones = [z for z in zones if z.get('active', False)]
            demand_count = sum(1 for z in zones if z['type'] == 'demand')
            supply_count = sum(1 for z in zones if z['type'] == 'supply')
            self.log_info(f"{tf.upper()}: {len(data.get('historical_data', []))} candles, "
                        f"D{demand_count} S{supply_count} zones, {len(active_zones)} active")
        self.log_info(f"{'='*60}")
        
        self.save_output(display_symbol)

    def save_output(self, symbol: str):
        try:
            output_dir = '/var/www/html/output'
            if not os.path.exists(output_dir):
                os.makedirs(output_dir)
            
            serializable_data = self.convert_to_serializable(self.output_data)
            
            timestamp = datetime.now(self.ist).strftime('%Y%m%d_%H%M%S')
            filename = f'{output_dir}/{symbol}_{timestamp}.json'
            
            with open(filename, 'w') as f:
                json.dump(serializable_data, f, indent=2)
            
            self.log_info(f"✅ Output saved to {filename}")
            
            latest_file = f'{output_dir}/{symbol}_latest.json'
            with open(latest_file, 'w') as f:
                json.dump(serializable_data, f, indent=2)
            
            self.log_info(f"✅ Latest data saved to {latest_file}")
            
            self.cleanup_old_files(output_dir, symbol)
            
        except Exception as e:
            self.log_debug(f"Error saving output: {e}")

if __name__ == "__main__":
    print("[INFO] Script started.")
    
    if not acquire_lock():
        print("Another instance is running. Exiting.")
        sys.exit(1)
    
    try:
        print("[INFO] Lock acquired, proceeding with execution.")
        
        client_details = ['DB1485', 'ABcd#1234', '14121985', 'DB1485_API', 'd9ce8d1261a834458847929c11ea1047']
        symbol = sys.argv[1] if len(sys.argv) > 1 else 'nifty'
        
        stock_fetcher = StockDataFetcher(client_details)
        stock_fetcher.login()
        stock_fetcher.fetch_all_data(symbol)
        print("[INFO] Execution completed.")
        
    except Exception as e:
        print("[ERROR] Exception occurred:", e)
        raise
    finally:
        release_lock()
        print("[INFO] Lock released. Script finished.")