import copy
from datetime import datetime
from typing import List

from loguru import logger

from optrabot.broker.optionpricedata import OptionStrikeData
from optrabot.broker.order import (Leg, OptionRight, Order, OrderAction,
                                   OrderType, PriceEffect)
from optrabot.managedtrade import ManagedTrade
from optrabot.optionhelper import OptionHelper
from optrabot.signaldata import SignalData
from optrabot.tradetemplate.processor.templateprocessorbase import \
    TemplateProcessorBase
from optrabot.tradetemplate.templatefactory import IronCondor, Template


class IronCondorProcessor(TemplateProcessorBase):
	def __init__(self, template: Template):
		"""
		Initializes the Iron Condor Processor with the given template
		"""
		super().__init__(template)
		self.credit_legs = None
		self.credit_legs_price = None
		self.adjustment_mode = None  # 'normal' or 'alternate' - tracks which side we're adjusting

	def composeEntryOrder(self, signalData: SignalData = None):
		"""
		Composes the entry order for the Iron Condor template
		"""
		super().composeEntryOrder(signalData)
		iron_condor_template :IronCondor = self._template
		short_strike_put = None
		short_strike_call = None
		premium = iron_condor_template.get_premium()

		# Short Strike Determination
		shortStrikeData = iron_condor_template.getShortStrikeData()
		if not shortStrikeData and not premium:
			raise ValueError('Configuration for Short Strike or premium is missing in template!')

		if iron_condor_template.wing == None:
				raise ValueError('Wing size has not been defined!')

		if shortStrikeData:
			if shortStrikeData.offset:
				logger.debug(f'Using Short Strike Offset: {shortStrikeData.offset}')
				short_strike_put =  OptionHelper.roundToStrikePrice(signalData.close - shortStrikeData.offset)
				short_strike_call =  OptionHelper.roundToStrikePrice(signalData.close + shortStrikeData.offset)
			elif shortStrikeData.delta:
				logger.debug(f'Using Short Strike Delta: {shortStrikeData.delta}')
				try:
					short_strike_put = self.get_short_strike_from_delta("SPX", OptionRight.PUT, shortStrikeData.delta)
					short_strike_call = self.get_short_strike_from_delta("SPX", OptionRight.CALL, shortStrikeData.delta)
				except ValueError as value_error:
					logger.error(f'Error while determining Short Strike by Delta: {value_error}')
					return None

			logger.debug(f'Using Wing Size: {iron_condor_template.wing}')
			long_strike_put = self.get_valid_strike(short_strike_put - iron_condor_template.wing, expiration_date=self._determined_expiration_date, higher=True)
			long_strike_call = self.get_valid_strike(short_strike_call + iron_condor_template.wing, expiration_date=self._determined_expiration_date, higher=False)

			# Now create the entry order with its legs as all required strikes are determined
			legs: List[Leg] = []
			legs.append(Leg(action=OrderAction.SELL, strike=short_strike_put, symbol=self._template.symbol, right=OptionRight.PUT, expiration=self._determined_expiration_date, quantity=1))
			legs.append(Leg(action=OrderAction.SELL, strike=short_strike_call, symbol=self._template.symbol, right=OptionRight.CALL, expiration=self._determined_expiration_date, quantity=1))
			legs.append(Leg(action=OrderAction.BUY, strike=long_strike_call, symbol=self._template.symbol, right=OptionRight.CALL, expiration=self._determined_expiration_date, quantity=1))
			legs.append(Leg(action=OrderAction.BUY, strike=long_strike_put, symbol=self._template.symbol, right=OptionRight.PUT, expiration=self._determined_expiration_date, quantity=1))
		elif premium:
			logger.debug(f'Trying to create an Iron Condor with premium: ${premium} and Wing Size: {iron_condor_template.wing}')
			self.credit_legs = None
			self.credit_legs_price = None
			self.adjustment_mode = 'normal'
			legs = self.compose_legs_with_min_premium(iron_condor_template.symbol, self._determined_expiration_date, premium, iron_condor_template.wing)
			if not legs:
				raise ValueError('Unable to determine legs for Iron Condor based on premium and wing size!')

		entryOrder = Order(symbol=self._template.symbol, legs=legs, action=OrderAction.BUY_TO_OPEN, quantity=self._template.amount, type=OrderType.LIMIT)
		return entryOrder
	
	def calculate_position_delta_and_price(self, legs: List[Leg], option_strike_data: OptionStrikeData) -> float:
		"""
		Calculates the total delta of the given legs and the mid price of the legs
		"""
		leg_delta = 0.0
		position_delta = 0.0
		for leg in legs:
			if leg.right == OptionRight.CALL:
				leg_delta = option_strike_data.strikeData[leg.strike].get_call_delta() * leg.quantity
				leg.midPrice = option_strike_data.strikeData[leg.strike].getCallMidPrice()
			elif leg.right == OptionRight.PUT:
				leg_delta = option_strike_data.strikeData[leg.strike].get_put_delta() * leg.quantity
				leg.midPrice = option_strike_data.strikeData[leg.strike].getPutMidPrice()
			if leg.action == OrderAction.SELL:
				leg_delta *= -1
			position_delta += leg_delta
		return position_delta
	
	def calculate_position_price(self, legs: List[Leg], open: bool = True) -> float:
		"""
		Calculates the total price of the given legs.
		- Positive position prices indicate a net credit (income)
		- Negative position prices indicate a net debit (cost)
		"""
		total_price = 0.0
		for leg in legs:
			if leg.action == OrderAction.BUY:
				if open:
					total_price -= leg.midPrice * leg.quantity
				else:
					total_price += leg.midPrice * leg.quantity
			elif leg.action == OrderAction.SELL:
				if open:
					total_price += leg.midPrice * leg.quantity
				else:
					total_price -= leg.midPrice * leg.quantity
		return total_price
	
	def compose_legs_with_min_premium(self, symbol: str, expiration_date: datetime.date, min_premium: float, wing: float) -> List[Leg] | None:
		"""
		Composes the Iron Condor legs based on the given minimum premium and the wing size.
		"""
		atm_strike = self.broker_connector.get_atm_strike(self._template.symbol)
		if not atm_strike:
			return
		
		current_short_call_strike = atm_strike
		current_short_put_strike = atm_strike
		option_strike_data = self.broker_connector.get_option_strike_data(symbol, expiration_date)
		strikes = option_strike_data.strikeData.keys()
		next_short_call_strike = OptionHelper.get_next_strike(current_short_call_strike, strikes, True)
		next_short_put_strike = OptionHelper.get_next_strike(current_short_put_strike, strikes, False)
		logger.debug(f'ATM Strike: {atm_strike} - starting with Put Strike: {next_short_put_strike} Call Strike: {next_short_call_strike}')

		starting_legs: List[Leg] = []
		starting_legs.append(Leg(action=OrderAction.SELL, strike=next_short_put_strike, symbol=symbol, right=OptionRight.PUT, expiration=expiration_date, quantity=1))
		starting_legs.append(Leg(action=OrderAction.SELL, strike=next_short_call_strike, symbol=symbol, right=OptionRight.CALL, expiration=expiration_date, quantity=1))
		long_strike_put = self.get_valid_strike(next_short_put_strike - self._template.wing, expiration_date, True)
		long_strike_call = self.get_valid_strike(next_short_call_strike + self._template.wing, expiration_date, False)
		starting_legs.append(Leg(action=OrderAction.BUY, strike=long_strike_call, symbol=self._template.symbol, right=OptionRight.CALL, expiration=expiration_date, quantity=1))
		starting_legs.append(Leg(action=OrderAction.BUY, strike=long_strike_put, symbol=self._template.symbol, right=OptionRight.PUT, expiration=expiration_date, quantity=1))

		entry_order_legs = self.determine_order_legs(starting_legs, strikes, option_strike_data, min_premium)

		return entry_order_legs
	
	def determine_order_legs(self, legs: List[Leg], strikes: List[float], option_strike_data, min_premium: float) -> List[Leg] | None:	
		"""
		Determines the according order legs recursively.
		
		The algorithm iterates through strike combinations trying to find a position that:
		1. Has a delta within the acceptable range (±2)
		2. Has a premium >= min_premium
		
		Strategy:
		- Normal mode: Move the overweight side further OTM to balance delta
		- Alternate mode: Move the other side closer to ATM to balance delta while keeping premium above min
		
		The algorithm stores any valid position (delta in range, premium >= min_premium) and continues
		searching for a position with lower premium but still >= min_premium.
		"""
		position_delta = round(self.calculate_position_delta_and_price(legs, option_strike_data) * 100, 2)
		logger.debug(f'Calculated Position Delta: {position_delta}')
		
		delta_in_range = -2 < position_delta < 2
		
		if delta_in_range:
			# Delta is OK, check if price is sufficient
			new_position_price = self.calculate_position_price(legs, True)
			logger.debug(f'Calculated Position Price: {new_position_price:.2f}. Required min price: {min_premium:.2f}')
			
			if new_position_price >= min_premium:
				# Valid position found - store it as the best candidate so far
				# (closer to min_premium than previous stored position)
				if self.credit_legs is None or new_position_price < self.credit_legs_price:
					self.credit_legs = copy.deepcopy(legs)
					self.credit_legs_price = new_position_price
					logger.debug(f'Stored valid position with premium ${new_position_price:.2f}')
				
				# Try to adjust further OTM to get closer to min_premium
				return self.adjust_legs(position_delta, legs, strikes, option_strike_data, min_premium)
			else:
				# Premium dropped below min_premium - return the last stored valid position
				if self.credit_legs is not None:
					logger.debug(f'Premium ${new_position_price:.2f} dropped below min ${min_premium:.2f}. '
								f'Using stored position with premium ${self.credit_legs_price:.2f}')
					return self.credit_legs
				else:
					# No valid position found at all - this shouldn't happen normally
					logger.warning(f'No valid position with premium >= ${min_premium:.2f} found.')
					return None
		else:
			# Delta out of range, need to adjust
			return self.adjust_legs(position_delta, legs, strikes, option_strike_data, min_premium)

	def adjust_legs(self, position_delta: float, legs: List[Leg], strikes: list[float], option_strike_data, min_premium: float) -> List[Leg] | None:
		"""
		Adjusts the legs to balance delta.
		
		In 'normal' mode: Moves the overweight side further OTM
		In 'alternate' mode: Moves the other side closer to ATM to compensate
		
		If normal adjustment causes delta to go out of range and we have a stored valid position,
		we try alternate mode instead.
		"""
		if self.adjustment_mode == 'normal':
			# Normal mode: Move overweight side further OTM
			result = self._adjust_legs_normal(position_delta, legs, strikes, option_strike_data, min_premium)
			if result is not None:
				return result
			
			# Normal adjustment failed (no more strikes or delta went out of range)
			# If we have a stored valid position, try alternate mode
			if self.credit_legs is not None:
				logger.debug(f'Normal adjustment exhausted. Trying alternate adjustment from stored position.')
				self.adjustment_mode = 'alternate'
				# Start fresh from the stored valid position
				alternate_legs = copy.deepcopy(self.credit_legs)
				# Recalculate delta for stored position
				stored_delta = round(self.calculate_position_delta_and_price(alternate_legs, option_strike_data) * 100, 2)
				return self._adjust_legs_alternate(stored_delta, alternate_legs, strikes, option_strike_data, min_premium)
			else:
				return None
		else:
			# Alternate mode
			return self._adjust_legs_alternate(position_delta, legs, strikes, option_strike_data, min_premium)
	
	def _adjust_legs_normal(self, position_delta: float, legs: List[Leg], strikes: list[float], option_strike_data, min_premium: float) -> List[Leg] | None:
		"""
		Normal adjustment: Move the overweight side further OTM to balance delta.
		"""
		if position_delta > 0:
			# Short Put leg need to be further OTM (lower strike)
			for leg in legs:
				if leg.right == OptionRight.PUT and leg.action == OrderAction.SELL:
					next_strike = OptionHelper.get_next_strike(leg.strike, strikes, False)
					if next_strike is not None:
						leg.strike = next_strike
						logger.debug(f'[Normal] Adjusted Short Put to Strike: {next_strike}')
						
						long_leg = next((l for l in legs if l.right == OptionRight.PUT and l.action == OrderAction.BUY), None)
						if long_leg is not None:
							long_leg.strike = self.get_valid_strike(next_strike - self._template.wing, expiration_date=self._determined_expiration_date, higher=True)
							logger.debug(f'[Normal] Adjusted Long Put to Strike: {long_leg.strike}')
					else:
						logger.debug('No further OTM Put Strike found.')
						return None
		else:
			# Short Call leg need to be further OTM (higher strike)
			for leg in legs:
				if leg.right == OptionRight.CALL and leg.action == OrderAction.SELL:
					next_strike = OptionHelper.get_next_strike(leg.strike, strikes, True)
					if next_strike is not None:
						leg.strike = next_strike
						logger.debug(f'[Normal] Adjusted Short Call to Strike: {next_strike}')
						
						long_leg = next((l for l in legs if l.right == OptionRight.CALL and l.action == OrderAction.BUY), None)
						if long_leg is not None:
							long_leg.strike = self.get_valid_strike(next_strike + self._template.wing, expiration_date=self._determined_expiration_date, higher=False)
							logger.debug(f'[Normal] Adjusted Long Call to Strike: {long_leg.strike}')
					else:
						logger.debug('No further OTM Call Strike found.')
						return None
		
		return self.determine_order_legs(legs, strikes, option_strike_data, min_premium)
	
	def _adjust_legs_alternate(self, position_delta: float, legs: List[Leg], strikes: list[float], option_strike_data, min_premium: float) -> List[Leg] | None:
		"""
		Alternate adjustment: Move the OTHER side closer to ATM to balance delta.
		This increases premium while adjusting delta.
		
		If delta > 0 (put-heavy): Move Call side closer to ATM (lower call strike)
		If delta < 0 (call-heavy): Move Put side closer to ATM (higher put strike)
		"""
		# First check if current position is valid
		delta_in_range = -2 < position_delta < 2
		if delta_in_range:
			position_price = self.calculate_position_price(legs, True)
			if position_price >= min_premium:
				# Found a valid position in alternate mode
				logger.debug(f'[Alternate] Found valid position with delta {position_delta:.2f} and premium ${position_price:.2f}')
				# Store if better than previous
				if self.credit_legs is None or position_price < self.credit_legs_price:
					self.credit_legs = copy.deepcopy(legs)
					self.credit_legs_price = position_price
				# Continue to see if we can get closer to min_premium
		
		if position_delta > 0:
			# Put-heavy: Move Call side closer to ATM (lower strike = more premium from call side)
			for leg in legs:
				if leg.right == OptionRight.CALL and leg.action == OrderAction.SELL:
					# Move to lower (closer to ATM) call strike
					next_strike = OptionHelper.get_next_strike(leg.strike, strikes, False)  # False = lower strike
					if next_strike is not None:
						# Don't cross over ATM
						atm_strike = self.broker_connector.get_atm_strike(self._template.symbol)
						if next_strike <= atm_strike:
							logger.debug(f'[Alternate] Cannot move Call strike below ATM ({atm_strike})')
							return self.credit_legs
						
						leg.strike = next_strike
						logger.debug(f'[Alternate] Adjusted Short Call closer to ATM: {next_strike}')
						
						long_leg = next((l for l in legs if l.right == OptionRight.CALL and l.action == OrderAction.BUY), None)
						if long_leg is not None:
							long_leg.strike = self.get_valid_strike(next_strike + self._template.wing, expiration_date=self._determined_expiration_date, higher=False)
							logger.debug(f'[Alternate] Adjusted Long Call to Strike: {long_leg.strike}')
					else:
						logger.debug('[Alternate] No further ITM Call Strike found.')
						return self.credit_legs
		else:
			# Call-heavy: Move Put side closer to ATM (higher strike = more premium from put side)
			for leg in legs:
				if leg.right == OptionRight.PUT and leg.action == OrderAction.SELL:
					# Move to higher (closer to ATM) put strike
					next_strike = OptionHelper.get_next_strike(leg.strike, strikes, True)  # True = higher strike
					if next_strike is not None:
						# Don't cross over ATM
						atm_strike = self.broker_connector.get_atm_strike(self._template.symbol)
						if next_strike >= atm_strike:
							logger.debug(f'[Alternate] Cannot move Put strike above ATM ({atm_strike})')
							return self.credit_legs
						
						leg.strike = next_strike
						logger.debug(f'[Alternate] Adjusted Short Put closer to ATM: {next_strike}')
						
						long_leg = next((l for l in legs if l.right == OptionRight.PUT and l.action == OrderAction.BUY), None)
						if long_leg is not None:
							long_leg.strike = self.get_valid_strike(next_strike - self._template.wing, expiration_date=self._determined_expiration_date, higher=True)
							logger.debug(f'[Alternate] Adjusted Long Put to Strike: {long_leg.strike}')
					else:
						logger.debug('[Alternate] No further ITM Put Strike found.')
						return self.credit_legs
		
		# Recalculate delta after adjustment
		new_delta = round(self.calculate_position_delta_and_price(legs, option_strike_data) * 100, 2)
		
		# Check if still valid
		if -2 < new_delta < 2:
			position_price = self.calculate_position_price(legs, True)
			if position_price >= min_premium:
				logger.debug(f'[Alternate] Valid position: delta {new_delta:.2f}, premium ${position_price:.2f}')
				if self.credit_legs is None or position_price < self.credit_legs_price:
					self.credit_legs = copy.deepcopy(legs)
					self.credit_legs_price = position_price
				# Continue searching for better position
				return self._adjust_legs_alternate(new_delta, legs, strikes, option_strike_data, min_premium)
			else:
				# Premium dropped below min - return stored position
				logger.debug(f'[Alternate] Premium ${position_price:.2f} dropped below min ${min_premium:.2f}')
				return self.credit_legs
		else:
			# Delta went out of range - return stored position
			logger.debug(f'[Alternate] Delta {new_delta:.2f} out of range')
			return self.credit_legs

	def composeTakeProfitOrder(self, managedTrade: ManagedTrade, fillPrice: float) -> Order:
		"""
		Composes the take profit order based on the template and the given fill price
		"""
		super().composeTakeProfitOrder(managedTrade, fillPrice)
		logger.debug('Creating take profit order for template {}', self._template.name)
		takeProfitPrice = self._template.calculateTakeProfitPrice(fillPrice)
		logger.debug(f'Calculated take profit price: {takeProfitPrice}')
		tp_order_legs = copy.deepcopy(managedTrade.entryOrder.legs)
		self.invert_leg_actions(tp_order_legs)
		takeProfitOrder = Order(symbol=self._template.symbol, legs=tp_order_legs, action=OrderAction.SELL_TO_CLOSE, quantity=self._template.amount, type=OrderType.LIMIT, price=takeProfitPrice)
		return takeProfitOrder
	
	def composeStopLossOrder(self, managedTrade: ManagedTrade, fillPrice: float) -> Order:
		"""
		Composes the stop loss order based on the template and the given fill price
		"""
		super().composeStopLossOrder(managedTrade, fillPrice)
		logger.debug('Creating stop loss order for template {}', self._template.name)
		stopLossPrice = self._template.calculateStopLossPrice(fillPrice)
		sl_order_legs = copy.deepcopy(managedTrade.entryOrder.legs)
		self.invert_leg_actions(sl_order_legs)
		logger.debug(f'Calculated stop loss price: {stopLossPrice}')
		stopLossOrder = Order(symbol=self._template.symbol, legs=sl_order_legs, action=OrderAction.SELL_TO_CLOSE, quantity=self._template.amount, type=OrderType.STOP, price=stopLossPrice)
		return stopLossOrder