import array
from collections import OrderedDict
import asyncio
import datetime as dt
import json
from fastapi import FastAPI
from ib_async import *
import logging
import webbrowser
from loguru import logger
import pytz
from sqlalchemy.orm import Session
from optrabot.broker.brokerconnector import BrokerConnector
from optrabot import schemas
from optrabot.broker.brokerfactory import BrokerFactory
from optrabot.marketdatatype import MarketDataType
from optrabot.optionhelper import OptionHelper
import optrabot.config as optrabotcfg
from optrabot.trademanager import TradeManager
from optrabot.tradetemplate.templatefactory import Template
import optrabot.symbolinfo as symbolInfo
from optrabot.tradetemplate.templatetrigger import TriggerType
from optrabot.tradetemplate.processor.templateprocessor import TemplateProcessor
from optrabot.signaldata import SignalData
from .tradinghubclient import TradinghubClient
from .timesync import TimeSync
import pkg_resources
from .database import *
from . import crud
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.schedulers.base import SchedulerNotRunningError

optrabot_dev_version = '0.18.1-dev'

def get_version() -> str:
	"""
	Returns the version of the package
	"""
	environment = os.environ.get('OPTRABOT_ENV', 'PROD')
	if environment != 'DEV':
		try:
			return pkg_resources.get_distribution('optrabot').version
		except pkg_resources.DistributionNotFound:
			return optrabot_dev_version 
	else:
		return optrabot_dev_version

class OptraBot():
	def __init__(self, app: FastAPI):
		self.app = app
		self._apiKey = None
		self.thc : TradinghubClient = None
		self._marketDataType : MarketDataType = None
		self.Version = get_version()
		self._backgroundScheduler = AsyncIOScheduler()
		self._backgroundScheduler.start()
		self._timeSync : TimeSync = None
			
	def __setitem__(self, key, value):
		setattr(self, key, value)

	def __getitem__(self, key):
		return getattr(self, key)
	
	async def startup(self):
		logger.info('OptraBot {version}', version=self.Version)
		# Read Config
		conf = optrabotcfg.Config("config.yaml")
		optrabotcfg.appConfig = conf
		self['config'] = conf
		conf.logConfigurationData()
		conf.readTemplates()
		conf.readFlows()
		updateDatabase()
		self.thc = TradinghubClient(self)
		if self.thc._apiKey == None:
			return

		try:
			additional_data = {
				'instance_id': conf.getInstanceId(),
				'accounts': self._getConfiguredAccounts()
			}
			await self.thc.connect(additional_data)
		except Exception as excp:
			logger.error('Problem on Startup: {}', excp)
			logger.error('OptraBot halted!')
			return
		
		logger.info('Sucessfully connected to OptraBot Hub')
		
		# OTB-253: createBrokerConnectors() now handles Trade Recovery
		# Trade Recovery runs after first broker connection (see BrokerFactory._onBrokerConnected)
		await BrokerFactory().createBrokerConnectors()
		
		self.thc.start_polling(self._backgroundScheduler)
		TradeManager()

		# Initialize FlowEngine
		from optrabot.flowengine import FlowEngine
		flow_engine = FlowEngine()
		flow_engine.initialize(conf.getFlows(), self._backgroundScheduler)

		self._backgroundScheduler.add_job(self._statusInfo, 'interval', minutes=5, id='statusInfo', misfire_grace_time=None)
		self._backgroundScheduler.add_job(self._new_day_start, 'cron', hour=0, minute=0, second=0, timezone=pytz.timezone('US/Eastern'), id='day_change', misfire_grace_time=None)
		self._backgroundScheduler.add_job(self._check_price_data, 'interval', seconds=30, id='check_price_data', misfire_grace_time=None)
		
		# OTB-258: Initialize time synchronization
		self._timeSync = TimeSync(self.thc)
		# Run immediately at startup
		self._backgroundScheduler.add_job(
			self._timeSync.check_and_sync_time,
			'date',
			run_date=dt.datetime.now() + dt.timedelta(seconds=5),
			id='time_sync_startup',
			misfire_grace_time=None
		)
		# Run daily at 6:00 AM Eastern Time
		self._backgroundScheduler.add_job(
			self._timeSync.check_and_sync_time,
			'cron',
			hour=6,
			minute=0,
			second=0,
			timezone=pytz.timezone('US/Eastern'),
			id='time_sync_daily',
			misfire_grace_time=None
		)
		
		self._scheduleTimeTriggers()

		environment = os.environ.get('OPTRABOT_ENV', 'PROD')
		if environment == 'DEV':
			logger.warning('Run the UI by command: npm start')
		else:
			webPort = conf.get('general.port')
			url = f"http://localhost:{webPort}/ui/optrabot.html"
			self._backgroundScheduler.add_job(self._open_browser, 'date', run_date=dt.datetime.now() + dt.timedelta(seconds=2), args=[url], id='open_browser', misfire_grace_time=None)
	
	async def _open_browser(self, url: str):
		"""
		Opens the default web browser to the specified URL
		"""
		try:
			webbrowser.open(url)
		except Exception as e:
			logger.warning(f'Error opening web browser: {e}')

	async def shutdown(self):
		logger.info('Shutting down OptraBot')
		
		# Shutdown FlowEngine
		try:
			from optrabot.flowengine import FlowEngine
			FlowEngine().shutdown()
		except Exception as e:
			logger.warning(f'Error shutting down FlowEngine: {e}')
		
		# Shutdown TradinghubClient
		try:
			if hasattr(self, 'thc') and self.thc is not None:
				await self.thc.shutdown()
		except Exception as e:
			logger.warning(f'Error shutting down TradinghubClient: {e}')
		
		# Shutdown TradeManager
		try:
			TradeManager().shutdown()
		except Exception as e:
			logger.warning(f'Error shutting down TradeManager: {e}')
		
		# Shutdown BrokerFactory
		try:
			await BrokerFactory().shutdownBrokerConnectors()
		except Exception as e:
			logger.warning(f'Error shutting down BrokerConnectors: {e}')
		
		# Shutdown Background Scheduler
		try:
			if hasattr(self, '_backgroundScheduler') and self._backgroundScheduler is not None:
				try:
					if hasattr(self._backgroundScheduler, 'running') and self._backgroundScheduler.running:
						self._backgroundScheduler.shutdown()
						logger.debug('OptraBot background scheduler shutdown completed')
					else:
						logger.debug('OptraBot background scheduler is not running, skipping shutdown')
				except SchedulerNotRunningError:
					logger.debug('OptraBot background scheduler already stopped')
				except Exception as e:
					logger.warning(f'Error shutting down OptraBot background scheduler: {e}')
		except Exception as e:
			logger.warning(f'Error during OptraBot background scheduler shutdown: {e}')
		
		logger.info('OptraBot shutdown completed')

	async def _check_price_data(self):
		"""
		Checks if the connected brokers are still delivering price data during the trading session
		"""
		await BrokerFactory().check_price_data()

	async def _new_day_start(self):
		"""
		Perform operations on start of a new day
		"""
		logger.debug('Performing Day Change operations')
		await BrokerFactory().new_day_start()
		self._scheduleTimeTriggers()

	def _statusInfo(self):
		siHubConnection = 'OK' if self.thc.isHubConnectionOK() == True else 'Problem!'

		managedTrades = TradeManager().getManagedTrades()
		activeTrades = 0
		for managedTrade in managedTrades:
			if managedTrade.isActive():
				activeTrades += 1

		logger.info(f'Broker Trading enabled: {BrokerFactory().get_trading_satus_info()}')
		logger.info(f'Status Info: Hub Connection: {siHubConnection} - Active Trades: {activeTrades}')

	def _scheduleTimeTriggers(self):
		"""
		Schedules the time triggers for the relevant templates with the time trigger
		"""
		conf: Config = self['config']
		now = dt.datetime.now().astimezone(pytz.UTC)
		for item in conf.getTemplates():
			template : Template = item
			trigger = template.getTrigger()
			if trigger is None:
				continue
			if trigger.type == TriggerType.Time:
				if template.is_enabled() == False:
					logger.debug(f'Template {template.name} is disabled. Not scheduling time trigger.')
					continue
				if trigger._weekdays:
					if now.weekday() not in trigger._weekdays:
						logger.debug(f'Template {template.name} is not scheduled for today')
						continue
				if trigger._blackout_days:
					if now.date() in trigger._blackout_days:
						logger.debug(f'Template {template.name} is in blackout days for today')
						continue
				if not BrokerFactory().is_trading_day():
					logger.debug(f'Market is not open. Not scheduling time trigger for template {template.name}')
					continue
				if trigger._trigger_time.time() < now.time():
					logger.debug(f'Trigger time {trigger._trigger_time} for template {template.name} is in the past. Not scheduling it.')
					continue

				trigger_time_today = dt.datetime.combine(now.date(), trigger._trigger_time.time(), tzinfo=trigger._trigger_time.tzinfo)
				logger.debug(f'Scheduling one-time trigger for template {template.name} at {trigger_time_today}')
				self._backgroundScheduler.add_job(
					self._triggerTemplate,
					'date',
					run_date=trigger_time_today,
					timezone=trigger._trigger_time.tzinfo,
					args=[template],
					id=f'time_trigger_{template.name}',
					misfire_grace_time=None
				)

	async def _triggerTemplate(self, template: Template):
		logger.info(f'Executing Time Trigger for template {template.name}')
		job_id = 'processtemplate' + str(template.name)
		signal_data = SignalData(timestamp=dt.datetime.now().astimezone(pytz.UTC), close=0, strike=0 )
		templateProcessor = TemplateProcessor()
		# Wrap processTemplate call to handle exceptions and send notifications
		self._backgroundScheduler.add_job(
			self._processTemplateWithErrorHandling, 
			args=[templateProcessor, template, signal_data], 
			id=job_id, 
			max_instances=10, 
			misfire_grace_time=None
		)
	
	async def _processTemplateWithErrorHandling(self, templateProcessor, template, signalData):
		"""
		Wrapper for processTemplate that catches exceptions and sends user notifications
		"""
		try:
			await templateProcessor.processTemplate(template, signalData)
		except Exception as e:
			logger.error(f'Template processing failed for {template.name}: {e}')
			from optrabot.tradinghubclient import TradinghubClient, NotificationType
			try:
				await TradinghubClient().send_notification(
					NotificationType.ERROR,
					f'❌ *Template Processing Failed*\n'
					f'*Template:* {template.name}\n'
					f'*Strategy:* {template.strategy}\n'
					f'*Account:* {template.account}\n'
					f'*Error:* {str(e)}'
				)
			except Exception as notify_error:
				logger.error(f'Failed to send error notification: {notify_error}')

	def getMarketDataType(self) -> MarketDataType:
		""" Return the configured Market Data Type
		"""
		if self._marketDataType is None:
			config: Config = self['config']
			try:
				confMarketData = config.get('tws.marketdata')
			except KeyError as keyError:
				confMarketData = 'Delayed'
			self._marketDataType = MarketDataType()
			self._marketDataType.byString(confMarketData)
		return self._marketDataType

	def _getConfiguredAccounts(self) -> list:
		""" 
		Returns a list of configured accounts
		"""
		#conf: Config = self['config']
		conf: Config = optrabotcfg.appConfig
		configuredAccounts = None
		for item in conf.getTemplates():
			template : Template = item
			if configuredAccounts == None:
				configuredAccounts = [template.account]
			else:
				if not template.account in configuredAccounts:
					configuredAccounts.append(template.account)
		return configuredAccounts

	@logger.catch
	def handleTaskDone(self, task: asyncio.Task):
		if not task.cancelled():
			taskException = task.exception()
			if taskException != None:
				logger.error('Task Exception occured!')
				raise taskException