# Copyright (c) DevSecCode. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root.

"""One-click deploy orchestration service.

Supports pluggable providers (Cloud Run, Firebase Hosting, Vercel).
Tracks deploy state, emits SSE events via asyncio queues.
Integrates with secrets_service for env-var injection.
"""

from __future__ import annotations

import asyncio
import logging
import os
import subprocess
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, AsyncGenerator

from dsc_api.models.deploy_models import (
	DeployEvent,
	DeployInfo,
	DeployProvider,
	DeployRequest,
	DeployStatus,
)
from dsc_api.models.secrets_models import SecretEnvironment

logger = logging.getLogger(__name__)

# In-memory deploy state
_deploys: dict[str, dict[str, Any]] = {}
_event_queues: dict[str, list[asyncio.Queue[DeployEvent | None]]] = {}


def _now() -> str:
	return datetime.now(timezone.utc).isoformat()


def _emit(deploy_id: str, event: DeployEvent) -> None:
	"""Push an event to all listeners for this deploy."""
	for q in _event_queues.get(deploy_id, []):
		q.put_nowait(event)


def _make_event(deploy_id: str, status: DeployStatus, message: str, live_url: str | None = None) -> DeployEvent:
	return DeployEvent(
		deploy_id=deploy_id,
		status=status,
		message=message,
		timestamp=_now(),
		live_url=live_url,
	)


# ---------------------------------------------------------------------------
# Provider implementations
# ---------------------------------------------------------------------------


async def _deploy_cloudrun(deploy_id: str, state: dict[str, Any]) -> None:
	"""Deploy to Google Cloud Run."""
	workspace = state["workspace_path"]
	project_name = state["project_name"]
	region = state["region"]
	env_vars: dict[str, str] = state.get("env_vars") or {}

	# Build step
	state["status"] = DeployStatus.building
	_emit(deploy_id, _make_event(deploy_id, DeployStatus.building, "Building container image..."))

	service_name = project_name.lower().replace(" ", "-").replace("_", "-")[:63]
	dockerfile = state.get("dockerfile") or "Dockerfile"
	dockerfile_path = Path(workspace) / dockerfile

	# Auto-generate Dockerfile if missing
	if not dockerfile_path.exists():
		state["status"] = DeployStatus.building
		_emit(deploy_id, _make_event(deploy_id, DeployStatus.building, "No Dockerfile found — generating one..."))
		dockerfile_path.write_text(
			"FROM node:20-alpine\nWORKDIR /app\nCOPY . .\n"
			"RUN npm install --production 2>/dev/null || pip install -r requirements.txt 2>/dev/null || true\n"
			'EXPOSE 8080\nCMD ["npm", "start"]\n',
			encoding="utf-8",
		)

	# Build via Cloud Build
	build_cmd = [
		"gcloud", "builds", "submit",
		"--tag", f"gcr.io/{_gcp_project()}/{service_name}",
		"--quiet",
		workspace,
	]

	try:
		proc = await asyncio.create_subprocess_exec(
			*build_cmd,
			stdout=asyncio.subprocess.PIPE,
			stderr=asyncio.subprocess.STDOUT,
		)
		stdout, _ = await proc.communicate()
		if proc.returncode != 0:
			raise RuntimeError(f"Build failed:\n{stdout.decode()[-500:]}")
	except FileNotFoundError:
		raise RuntimeError("gcloud CLI not found — install Google Cloud SDK to deploy to Cloud Run")

	# Deploy step
	state["status"] = DeployStatus.deploying
	_emit(deploy_id, _make_event(deploy_id, DeployStatus.deploying, "Deploying to Cloud Run..."))

	deploy_cmd = [
		"gcloud", "run", "deploy", service_name,
		"--image", f"gcr.io/{_gcp_project()}/{service_name}",
		"--region", region,
		"--platform", "managed",
		"--allow-unauthenticated",
		"--quiet",
	]

	# Inject env vars
	if env_vars:
		env_str = ",".join(f"{k}={v}" for k, v in env_vars.items())
		deploy_cmd.extend(["--set-env-vars", env_str])

	try:
		proc = await asyncio.create_subprocess_exec(
			*deploy_cmd,
			stdout=asyncio.subprocess.PIPE,
			stderr=asyncio.subprocess.STDOUT,
		)
		stdout, _ = await proc.communicate()
		output = stdout.decode()
		if proc.returncode != 0:
			raise RuntimeError(f"Deploy failed:\n{output[-500:]}")

		# Parse URL from output
		live_url = None
		for line in output.splitlines():
			if "https://" in line:
				for word in line.split():
					if word.startswith("https://"):
						live_url = word.rstrip(".")
						break
				if live_url:
					break

		if not live_url:
			live_url = f"https://{service_name}-{_gcp_project()}.{region}.run.app"

		state["status"] = DeployStatus.live
		state["live_url"] = live_url
		state["updated_at"] = _now()
		_emit(deploy_id, _make_event(deploy_id, DeployStatus.live, f"Live at {live_url}", live_url))

	except FileNotFoundError:
		raise RuntimeError("gcloud CLI not found")


async def _deploy_firebase(deploy_id: str, state: dict[str, Any]) -> None:
	"""Deploy to Firebase Hosting."""
	workspace = state["workspace_path"]

	state["status"] = DeployStatus.building
	_emit(deploy_id, _make_event(deploy_id, DeployStatus.building, "Preparing Firebase Hosting deploy..."))

	try:
		proc = await asyncio.create_subprocess_exec(
			"firebase", "deploy", "--only", "hosting",
			cwd=workspace,
			stdout=asyncio.subprocess.PIPE,
			stderr=asyncio.subprocess.STDOUT,
		)
		stdout, _ = await proc.communicate()
		output = stdout.decode()
		if proc.returncode != 0:
			raise RuntimeError(f"Firebase deploy failed:\n{output[-500:]}")

		live_url = None
		for line in output.splitlines():
			if "Hosting URL:" in line:
				live_url = line.split("Hosting URL:")[-1].strip()
				break

		state["status"] = DeployStatus.live
		state["live_url"] = live_url or "https://your-project.web.app"
		state["updated_at"] = _now()
		_emit(deploy_id, _make_event(deploy_id, DeployStatus.live, f"Live at {state['live_url']}", state["live_url"]))

	except FileNotFoundError:
		raise RuntimeError("firebase CLI not found — run: npm install -g firebase-tools")


async def _deploy_vercel(deploy_id: str, state: dict[str, Any]) -> None:
	"""Deploy to Vercel."""
	workspace = state["workspace_path"]
	env_vars: dict[str, str] = state.get("env_vars") or {}

	state["status"] = DeployStatus.building
	_emit(deploy_id, _make_event(deploy_id, DeployStatus.building, "Deploying to Vercel..."))

	cmd = ["vercel", "--yes", "--prod"]
	for k, v in env_vars.items():
		cmd.extend(["-e", f"{k}={v}"])

	try:
		proc = await asyncio.create_subprocess_exec(
			*cmd,
			cwd=workspace,
			stdout=asyncio.subprocess.PIPE,
			stderr=asyncio.subprocess.STDOUT,
		)
		stdout, _ = await proc.communicate()
		output = stdout.decode()
		if proc.returncode != 0:
			raise RuntimeError(f"Vercel deploy failed:\n{output[-500:]}")

		live_url = output.strip().splitlines()[-1].strip()
		if not live_url.startswith("https://"):
			live_url = f"https://{live_url}"

		state["status"] = DeployStatus.live
		state["live_url"] = live_url
		state["updated_at"] = _now()
		_emit(deploy_id, _make_event(deploy_id, DeployStatus.live, f"Live at {live_url}", live_url))

	except FileNotFoundError:
		raise RuntimeError("vercel CLI not found — run: npm install -g vercel")


_PROVIDERS = {
	DeployProvider.cloudrun: _deploy_cloudrun,
	DeployProvider.firebase: _deploy_firebase,
	DeployProvider.vercel: _deploy_vercel,
}


def _gcp_project() -> str:
	"""Return the active GCP project ID."""
	return os.environ.get("GCLOUD_PROJECT", os.environ.get("GCP_PROJECT", "deva-deploy"))


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


async def start_deploy(request: DeployRequest) -> str:
	"""Start a deployment. Returns the deploy_id."""
	deploy_id = uuid.uuid4().hex[:12]
	now = _now()

	# Optionally inject secrets from vault
	env_vars = dict(request.env_vars or {})
	if request.inject_secrets:
		try:
			from dsc_api.services.secrets_service import get_secrets_service
			svc = get_secrets_service(request.workspace_path)
			if svc.is_unlocked:
				env_name = SecretEnvironment(request.secrets_environment)
				# We need the master password for injection — skip if vault locked
				vault_env = svc.list_secrets(env_name)
				for info in vault_env:
					val = svc.get_secret(info.key, info.environment)
					if val and info.key not in env_vars:
						env_vars[info.key] = val
		except Exception as exc:
			logger.warning("Could not inject secrets: %s", exc)

	state: dict[str, Any] = {
		"deploy_id": deploy_id,
		"project_name": request.project_name,
		"provider": request.provider,
		"status": DeployStatus.queued,
		"region": request.region,
		"workspace_path": request.workspace_path,
		"dockerfile": request.dockerfile,
		"env_vars": env_vars,
		"live_url": None,
		"error": None,
		"created_at": now,
		"updated_at": now,
	}
	_deploys[deploy_id] = state
	_event_queues[deploy_id] = []

	# Run deploy in background
	asyncio.create_task(_run_deploy(deploy_id))
	return deploy_id


async def _run_deploy(deploy_id: str) -> None:
	"""Execute the deploy pipeline."""
	state = _deploys[deploy_id]
	provider_fn = _PROVIDERS.get(state["provider"])

	if not provider_fn:
		state["status"] = DeployStatus.failed
		state["error"] = f"Unsupported provider: {state['provider']}"
		state["updated_at"] = _now()
		_emit(deploy_id, _make_event(deploy_id, DeployStatus.failed, state["error"]))
		_close_queues(deploy_id)
		return

	try:
		await provider_fn(deploy_id, state)
	except Exception as exc:
		state["status"] = DeployStatus.failed
		state["error"] = str(exc)
		state["updated_at"] = _now()
		_emit(deploy_id, _make_event(deploy_id, DeployStatus.failed, f"Deploy failed: {exc}"))
		logger.exception("Deploy %s failed", deploy_id)
	finally:
		_close_queues(deploy_id)


def _close_queues(deploy_id: str) -> None:
	"""Signal end-of-stream to all listeners."""
	for q in _event_queues.get(deploy_id, []):
		q.put_nowait(None)


def get_deploy(deploy_id: str) -> DeployInfo | None:
	"""Get deployment info by ID."""
	state = _deploys.get(deploy_id)
	if not state:
		return None
	return DeployInfo(
		deploy_id=state["deploy_id"],
		project_name=state["project_name"],
		provider=state["provider"],
		status=state["status"],
		region=state["region"],
		live_url=state.get("live_url"),
		error=state.get("error"),
		created_at=state["created_at"],
		updated_at=state["updated_at"],
	)


def list_deploys() -> list[DeployInfo]:
	"""List all deployments."""
	return [get_deploy(did) for did in _deploys if get_deploy(did) is not None]


async def subscribe_events(deploy_id: str) -> AsyncGenerator[DeployEvent, None]:
	"""Subscribe to SSE events for a deploy."""
	q: asyncio.Queue[DeployEvent | None] = asyncio.Queue()
	_event_queues.setdefault(deploy_id, []).append(q)
	try:
		while True:
			event = await q.get()
			if event is None:
				break
			yield event
	finally:
		_event_queues.get(deploy_id, []).remove(q) if q in _event_queues.get(deploy_id, []) else None


async def teardown(deploy_id: str) -> bool:
	"""Tear down a deployment. Returns True if successful."""
	state = _deploys.get(deploy_id)
	if not state:
		return False

	state["status"] = DeployStatus.torn_down
	state["updated_at"] = _now()
	logger.info("Deploy %s torn down", deploy_id)
	return True
