from asyncio import CancelledError, Event, TaskGroup
from logging import getLogger
from traceback import TracebackException

from asyncpg import Connection
from opentelemetry import trace
from pydantic import TypeAdapter

from .client import KyooClient
from .database import get_db
from .models.request import Request, RequestRet
from .models.videos import Resource
from .providers.provider import Provider, ProviderError

logger = getLogger(__name__)
tracer = trace.get_tracer("kyoo.scanner")


class RequestCreator:
	def __init__(self, database: Connection):
		self._database = database

	async def enqueue(self, requests: list[Request]):
		ret = await self._database.fetchmany(
			"""
			insert into scanner.requests(kind, title, year, external_id, videos)
				values ($1, $2, $3, $4, $5)
			on conflict (kind, title, year)
				do update set
					videos = requests.videos || excluded.videos
			returning
				pk::text as id
			""",
			[
				[x["kind"], x["title"], x["year"], x["external_id"], x["videos"]]
				for x in TypeAdapter(list[Request]).dump_python(requests)
			],
		)
		_ = await self._database.execute("notify scanner_requests")
		return [
			RequestRet(
				id=x["id"],
				kind=req.kind,
				title=req.title,
				year=req.year,
				status="pending",
				videos=[v.id for v in req.videos],
				error=None,
				started_at=None,
			)
			for req, x in zip(requests, ret)
		]

	async def clear_failed(self):
		_ = await self._database.execute(
			"""
			delete from scanner.requests
			where status = 'failed'
			"""
		)


class RequestProcessor:
	def __init__(
		self,
		client: KyooClient,
		providers: Provider,
	):
		self._database: Connection = None  # type: ignore
		self._client = client
		self._providers = providers
		self._processing = False

	@tracer.start_as_current_span("listen_requests")
	async def listen(self, tg: TaskGroup):
		closed = Event()

		def process(*_):
			_ = tg.create_task(self.process_all())

		def terminated(*_):
			closed.set()

		while True:
			closed.clear()
			async with get_db() as db:
				try:
					self._database = db
					self._database.add_termination_listener(terminated)
					await self._database.add_listener("scanner_requests", process)

					logger.info("Listening for requests")
					_ = await closed.wait()
					logger.info("stopping...")
				except CancelledError:
					logger.info("Stopped listening for requsets")
					await self._database.remove_listener("scanner_requests", process)
					self._database.remove_termination_listener(terminated)
					raise

	async def process_all(self):
		if self._processing:
			return
		self._processing = True
		try:
			found = True
			while found:
				try:
					found = await self.process_request()
				except Exception as e:
					logger.error(
						"Failed to process one of the metadata request", exc_info=e
					)
		finally:
			self._processing = False

	@tracer.start_as_current_span("process video")
	async def process_request(self):
		cur = await self._database.fetchrow(
			"""
			update
				scanner.requests
			set
				status = 'running',
				started_at = now()::timestamptz
			where
				pk in (
					select
						pk
					from
						scanner.requests
					where
						status = 'pending'
						or (
							status = 'running'
							and now() - started_at > interval '30 minutes'
						)
					limit 1
					for update
						skip locked)
			returning
				*
			"""
		)
		if cur is None:
			return False
		request = Request.model_validate(cur)

		span = trace.get_current_span()
		span.update_name(f"process {request.title}")
		logger.info(f"Starting to process {request.title}")
		try:
			show = await self._run_request(request)
			finished = await self._database.fetchrow(
				"""
				delete from scanner.requests
				where pk = $1
				returning
					videos
				""",
				request.pk,
			)
			if finished and finished["videos"] != request.videos:
				videos = TypeAdapter(list[Request.Video]).validate_python(
					finished["videos"]
				)
				await self._client.link_videos(
					"movie" if request.kind == "movie" else "serie",
					show.slug,
					videos,
				)
		except Exception as e:
			span.set_status(trace.Status(trace.StatusCode.ERROR))
			span.record_exception(e)
			if type(e) == ProviderError:
				logger.error(str(e))
			else:
				logger.error("Couldn't process request", exc_info=e)
			cur = await self._database.execute(
				"""
				update
					scanner.requests
				set
					status = 'failed',
					error = $2
				where
					pk = $1
				""",
				request.pk,
				{
					"title": type(e).__name__,
					"message": str(e),
					"traceback": [
						line
						for part in TracebackException.from_exception(e).format()
						for line in part.split("\n")
						if line.strip()
					],
				},
			)
		return True

	async def _run_request(self, request: Request) -> Resource:
		if request.kind == "movie":
			movie = await self._providers.find_movie(
				request.title,
				request.year,
				request.external_id,
			)
			movie.videos = [x.id for x in request.videos]
			return await self._client.create_movie(movie)

		serie = await self._providers.find_serie(
			request.title,
			request.year,
			request.external_id,
		)
		for vid in request.videos:
			for ep in vid.episodes:
				entry = next(
					(
						x
						for x in serie.entries
						if (ep.season is None and x.order == ep.episode)
						or (
							x.season_number == ep.season
							and x.episode_number == ep.episode
						)
					),
					None,
				)
				if entry is None:
					logger.warning(
						f"Couldn't match entry for {serie.slug} {ep.season or 'abs'}-e{ep.episode}."
					)
					continue
				entry.videos.append(vid.id)
		return await self._client.create_serie(serie)
