"""
OTB-387: Economic Calendar Service
===================================
Fetches economic calendar events from Investing.com using aiohttp
and caches them in the local SQLite database.
"""

import json
import random
from datetime import date, datetime, timedelta

import aiohttp
import pytz
from bs4 import BeautifulSoup
from loguru import logger
from sqlalchemy.orm import Session

from . import crud, models
from .database import SessionLocal

# ══════════════════════════════════════════════════════════════════════════════
# MAPPINGS
# ══════════════════════════════════════════════════════════════════════════════

COUNTRY_IDS = {
	'united states':  5,
	'germany':        17,
	'united kingdom': 4,
	'france':         22,
	'japan':          35,
	'china':          37,
	'canada':         6,
	'australia':      25,
	'switzerland':    12,
	'eurozone':       72,
}

IMPORTANCE_IDS = {'low': 1, 'medium': 2, 'high': 3}
IMPORTANCE_LABEL = {1: 'low', 2: 'medium', 3: 'high'}

TIMEZONE_IDS = {
	'GMT -5:00': 8,   # New York (EST)
	'GMT -4:00': 9,   # New York (EDT)
	'GMT':       19,
	'GMT +1:00': 55,  # Berlin/Vienna (CET)
	'GMT +2:00': 56,  # Berlin/Vienna (CEST)
}

# Reverse map: country_id -> country name
COUNTRY_NAMES = {v: k for k, v in COUNTRY_IDS.items()}

# Currency -> Country mapping (for enriching parsed events)
CURRENCY_COUNTRY = {
	'USD': 'united states',
	'EUR': 'eurozone',
	'GBP': 'united kingdom',
	'JPY': 'japan',
	'CNY': 'china',
	'CAD': 'canada',
	'AUD': 'australia',
	'CHF': 'switzerland',
}

URL = 'https://de.investing.com/economic-calendar/Service/getCalendarFilteredData'
INIT_URL = 'https://de.investing.com/economic-calendar/'

# German month names for parsing date strings from de.investing.com
GERMAN_MONTHS = {
	'Januar': 1, 'Februar': 2, 'März': 3, 'April': 4,
	'Mai': 5, 'Juni': 6, 'Juli': 7, 'August': 8,
	'September': 9, 'Oktober': 10, 'November': 11, 'Dezember': 12,
}

USER_AGENTS = [
	'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
	'(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
	'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3) AppleWebKit/605.1.15 '
	'(KHTML, like Gecko) Version/17.2 Safari/605.1.15',
	'Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0',
]

# Settings keys for dashboard preferences
SETTING_DASHBOARD_COUNTRIES = 'eco_calendar_dashboard_countries'
SETTING_DASHBOARD_IMPORTANCE = 'eco_calendar_dashboard_importance'
SETTING_TIMEZONE = 'eco_calendar_timezone'

# Defaults
DEFAULT_COUNTRIES = ['united states']
DEFAULT_IMPORTANCE = ['high']
DEFAULT_TIMEZONE = 'GMT +1:00'

# Always fetch from Investing.com in US Eastern timezone (ID 8).
# This is the only timezone ID verified to return correct times.
# Timezone ID 55 (labeled CET) was found to return UTC, not CET.
FETCH_TIMEZONE_ID = 8
SOURCE_TIMEZONE = 'America/New_York'


def _build_payload(from_date: date, to_date: date, importances: list,
                   countries: list, timezone: str) -> list:
	"""Build form-encoded payload for the Investing.com API."""
	# Always use FETCH_TIMEZONE_ID (US Eastern, verified correct)
	# instead of the user-configured timezone, which may map to
	# a broken Investing.com timezone ID.
	tz_id = FETCH_TIMEZONE_ID
	payload = [
		('timeZone',      str(tz_id)),
		('timeFilter',    'timeOnly'),
		('currentTab',    'custom'),
		('submitFilters', '1'),
		('limit_from',    '0'),
		('dateFrom',      from_date.strftime('%Y-%m-%d')),
		('dateTo',        to_date.strftime('%Y-%m-%d')),
	]
	for country in countries:
		cid = COUNTRY_IDS.get(country.lower())
		if cid:
			payload.append(('country[]', str(cid)))
	for imp in importances:
		iid = IMPORTANCE_IDS.get(imp.lower())
		if iid:
			payload.append(('importance[]', str(iid)))
	return payload


def _parse_events(html: str, event_date_fallback: date | None = None) -> list[dict]:
	"""Parse HTML response from Investing.com into a list of event dicts."""
	soup = BeautifulSoup(html, 'lxml')
	events = []
	current_time = ''
	current_date = ''

	for row in soup.select('tr'):
		# Detect date separator rows.
		# Investing.com puts the date in a <td class="theDay" id="theDay...">
		# inside a plain <tr> (the tr itself has no id).
		day_cell = row.select_one('td.theDay')
		if day_cell:
			current_date = day_cell.get_text(strip=True)
			continue

		if 'js-event-item' not in row.get('class', []):
			continue

		# Update current time if the cell is not empty
		time_cell = row.select_one('td.time')
		if time_cell and time_cell.get_text(strip=True):
			current_time = time_cell.get_text(strip=True)

		# Count filled bull icons to determine importance level (1-3)
		importance = len(row.select('i.grayFullBullishIcon'))

		name_cell = row.select_one('td.event a') or row.select_one('td.event')
		name = name_cell.get_text(strip=True) if name_cell else '-'

		curr_cell = row.select_one('td.flagCur')
		currency = curr_cell.get_text(strip=True) if curr_cell else ''

		# Determine country from currency
		country = CURRENCY_COUNTRY.get(currency, '')

		def cell_text(selector: str) -> str:
			el = row.select_one(selector)
			return el.get_text(strip=True) if el else ''

		events.append({
			'date':       current_date,
			'time':       current_time,
			'currency':   currency,
			'country':    country,
			'importance': importance,
			'event':      name,
			'actual':     cell_text('td.act'),
			'forecast':   cell_text('td.fore'),
			'previous':   cell_text('td.prev'),
		})

	return events


def _parse_date_string(date_str: str, fallback: date | None = None) -> date | None:
	"""Parse date strings from Investing.com into a date object.

	Supports both German (de.investing.com) and English formats:
	  - German: "Dienstag, 24. Februar 2026"
	  - English: "Monday, February 24, 2026"
	  - ISO: "2026-02-24"
	"""
	if not date_str:
		return fallback

	# Try German format: "Dienstag, 24. Februar 2026"
	import re
	de_match = re.match(r'\w+,\s+(\d{1,2})\.\s+(\w+)\s+(\d{4})', date_str)
	if de_match:
		day = int(de_match.group(1))
		month_name = de_match.group(2)
		year = int(de_match.group(3))
		month = GERMAN_MONTHS.get(month_name)
		if month:
			try:
				return date(year, month, day)
			except ValueError:
				pass

	# Try English formats
	for fmt in ('%A, %B %d, %Y', '%B %d, %Y', '%Y-%m-%d'):
		try:
			return datetime.strptime(date_str, fmt).date()
		except ValueError:
			continue
	return fallback


class EconomicCalendarService:
	"""Service for fetching and caching economic calendar events."""

	def __init__(self) -> None:
		self._last_fetch: datetime | None = None

	# ──────────────────────────────────────────────────────────────────────
	# Public API
	# ──────────────────────────────────────────────────────────────────────

	async def fetch_and_store(self,
	                          from_date: date,
	                          to_date: date,
	                          countries: list[str] | None = None,
	                          importances: list[str] | None = None,
	                          timezone: str | None = None) -> int:
		"""
		Fetch events from Investing.com and store them in the database.
		Returns the number of events stored.
		"""
		# Load settings from DB for defaults
		with SessionLocal() as session:
			if countries is None:
				countries = self._get_dashboard_countries(session)
			if importances is None:
				importances = self._get_dashboard_importance(session)
			if timezone is None:
				timezone = self._get_timezone(session)

		logger.info(
			'Fetching economic calendar: {} to {} | Countries: {} | Importance: {}',
			from_date, to_date, ', '.join(countries), ', '.join(importances)
		)

		try:
			html = await self._fetch_from_investing(from_date, to_date, importances, countries, timezone)
			events = _parse_events(html, event_date_fallback=from_date)
			logger.info('Parsed {} economic events', len(events))

			# Store in database
			with SessionLocal() as session:
				self._store_events(session, events, from_date, to_date)

			self._last_fetch = datetime.now(pytz.UTC)
			return len(events)

		except Exception as e:
			logger.error('Failed to fetch economic calendar: {}', e)
			return 0

	def get_dashboard_events(self, session: Session) -> list[models.EconomicEvent]:
		"""Get today's events filtered by dashboard settings."""
		today = date.today()
		countries = self._get_dashboard_countries(session)
		importances = self._get_dashboard_importance(session)
		importance_ids = [IMPORTANCE_IDS[i] for i in importances if i in IMPORTANCE_IDS]

		return crud.get_economic_events(
			session, today, today,
			countries=countries,
			importances=importance_ids
		)

	def get_events(self, session: Session,
	               from_date: date, to_date: date,
	               countries: list[str] | None = None,
	               importances: list[int] | None = None) -> list[models.EconomicEvent]:
		"""Get events from DB with optional filters."""
		return crud.get_economic_events(
			session, from_date, to_date,
			countries=countries,
			importances=importances
		)

	def get_settings(self, session: Session) -> dict:
		"""Read current calendar settings from DB."""
		return {
			'dashboard_countries': self._get_dashboard_countries(session),
			'dashboard_importance': self._get_dashboard_importance(session),
			'timezone': self._get_timezone(session),
		}

	def save_settings(self, session: Session, *,
	                   dashboard_countries: list[str] | None = None,
	                   dashboard_importance: list[str] | None = None,
	                   timezone: str | None = None) -> None:
		"""Save calendar settings to DB."""
		if dashboard_countries is not None:
			crud.set_setting(
				session, SETTING_DASHBOARD_COUNTRIES,
				json.dumps(dashboard_countries),
				'Economic calendar: countries shown on dashboard'
			)
		if dashboard_importance is not None:
			crud.set_setting(
				session, SETTING_DASHBOARD_IMPORTANCE,
				json.dumps(dashboard_importance),
				'Economic calendar: importance levels shown on dashboard'
			)
		if timezone is not None:
			crud.set_setting(
				session, SETTING_TIMEZONE,
				timezone,
				'Economic calendar: timezone for event times'
			)

	async def cleanup_old_events(self, days: int = 30) -> None:
		"""Remove events older than the given number of days."""
		cutoff = date.today() - timedelta(days=days)
		with SessionLocal() as session:
			deleted = crud.delete_old_economic_events(session, cutoff)
			if deleted > 0:
				logger.info('Cleaned up {} old economic calendar events', deleted)

	# ──────────────────────────────────────────────────────────────────────
	# Internal helpers
	# ──────────────────────────────────────────────────────────────────────

	async def _fetch_from_investing(self, from_date: date, to_date: date,
	                                 importances: list, countries: list,
	                                 timezone: str) -> str:
		"""Async HTTP fetch from Investing.com economic calendar API."""
		ua = random.choice(USER_AGENTS)

		async with aiohttp.ClientSession() as http_session:
			# Initialize session with German site (get cookies + German locale)
			async with http_session.get(
				INIT_URL,
				headers={'User-Agent': ua, 'Accept-Language': 'de-DE,de;q=0.9'},
				timeout=aiohttp.ClientTimeout(total=15),
			) as _init_resp:
				pass  # Just need the cookies

			headers = {
				'User-Agent':       ua,
				'X-Requested-With': 'XMLHttpRequest',
				'Accept':           'application/json, text/javascript, */*; q=0.01',
				'Accept-Language':  'de-DE,de;q=0.9',
				'Referer':          INIT_URL,
				'Content-Type':     'application/x-www-form-urlencoded',
			}

			payload = _build_payload(from_date, to_date, importances, countries, timezone)

			async with http_session.post(
				URL, headers=headers, data=payload,
				timeout=aiohttp.ClientTimeout(total=15),
			) as resp:
				resp.raise_for_status()
				data = await resp.json(content_type=None)

				if not data.get('data'):
					raise ValueError(f'Empty response from Investing.com: {str(data)[:300]}')
				return data['data']

	def _store_events(self, session: Session, events: list[dict],
	                   from_date: date, to_date: date) -> None:
		"""Store parsed events in the database, replacing existing ones for the date range."""
		db_events = []
		for ev in events:
			event_date = _parse_date_string(ev['date'], fallback=from_date)
			db_events.append(models.EconomicEvent(
				event_date=event_date,
				time=ev['time'],
				currency=ev['currency'],
				country=ev.get('country', ''),
				importance=ev['importance'],
				event_name=ev['event'],
				actual=ev['actual'],
				forecast=ev['forecast'],
				previous=ev['previous'],
				fetched_at=datetime.now(pytz.UTC),
			))

		crud.upsert_economic_events(session, db_events, from_date, to_date)

	def _get_dashboard_countries(self, session: Session) -> list[str]:
		"""Get dashboard countries from settings, with default fallback."""
		raw = crud.get_setting_value(session, SETTING_DASHBOARD_COUNTRIES)
		if raw:
			try:
				return json.loads(raw)
			except (json.JSONDecodeError, TypeError):
				pass
		return DEFAULT_COUNTRIES

	def _get_dashboard_importance(self, session: Session) -> list[str]:
		"""Get dashboard importance levels from settings, with default fallback."""
		raw = crud.get_setting_value(session, SETTING_DASHBOARD_IMPORTANCE)
		if raw:
			try:
				return json.loads(raw)
			except (json.JSONDecodeError, TypeError):
				pass
		return DEFAULT_IMPORTANCE

	def _get_timezone(self, session: Session) -> str:
		"""Get timezone from settings, with default fallback."""
		raw = crud.get_setting_value(session, SETTING_TIMEZONE)
		return raw if raw else DEFAULT_TIMEZONE
