import json
import os
import shutil
import subprocess
import tempfile

import frappe
import requests
import urllib3
import yaml

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

TEMPLATES_DIR = "/home/frappe/frappe-bench/apps/admin_panel/templates/distrobuilder"
META_FILE = os.path.join(TEMPLATES_DIR, "apps_meta.json")
SERVERS_FILE = "/home/frappe/frappe-bench/apps/admin_panel/templates/servers.json"
CERTS_DIR = "/home/frappe/frappe-bench/apps/admin_panel/certs"
CLIENT_CERT = os.path.join(CERTS_DIR, "client.crt")
CLIENT_KEY = os.path.join(CERTS_DIR, "client.key")


def _check_admin_permission():
	"""Check if user has admin permission (System Manager or Administrator)."""
	if frappe.session.user == "Guest":
		frappe.throw("Please login to access this feature", frappe.AuthenticationError)

	if frappe.session.user == "Administrator":
		return

	roles = frappe.get_roles()
	if "System Manager" not in roles and "Administrator" not in roles:
		frappe.throw(
			"You need System Manager or Administrator role to access this feature",
			frappe.PermissionError,
		)


def _ensure_templates_dir():
	"""Ensure templates directory exists."""
	if not os.path.exists(TEMPLATES_DIR):
		os.makedirs(TEMPLATES_DIR, exist_ok=True)


def _load_meta():
	"""Load apps metadata from JSON file."""
	_ensure_templates_dir()
	if os.path.exists(META_FILE):
		with open(META_FILE, "r") as f:
			return json.load(f)
	return {}


def _save_meta(meta):
	"""Save apps metadata to JSON file."""
	_ensure_templates_dir()
	with open(META_FILE, "w") as f:
		json.dump(meta, f, indent=2)


def _load_servers():
	"""Load servers config from JSON file."""
	if os.path.exists(SERVERS_FILE):
		with open(SERVERS_FILE, "r") as f:
			return json.load(f)
	return {}


def _get_image_server():
	"""Get the first configured image server URL."""
	servers = _load_servers()
	for name, config in servers.items():
		if config.get("is_image_server"):
			return {"name": name, "url": config.get("url", "")}
	# Fallback: return the first server with a URL
	for name, config in servers.items():
		if config.get("url"):
			return {"name": name, "url": config.get("url", "")}
	return None


def _incus_api(server_url, method, path, data=None, timeout=30):
	"""Make REST API request to an Incus server using panel certificate."""
	if not os.path.exists(CLIENT_CERT) or not os.path.exists(CLIENT_KEY):
		raise Exception("Panel certificate not configured. Generate one in Servers tab.")

	url = f"{server_url}{path}"
	r = requests.request(
		method,
		url,
		headers={"Content-Type": "application/json"},
		cert=(CLIENT_CERT, CLIENT_KEY),
		verify=False,
		json=data,
		timeout=timeout,
	)
	r.raise_for_status()
	return r.json()


def _get_server_url(server_name):
	"""Get URL for a specific server."""
	servers = _load_servers()
	config = servers.get(server_name, {})
	return config.get("url", "")


def _get_all_images(server_url):
	"""Get all images from a server (single request)."""
	try:
		resp = _incus_api(server_url, "GET", "/1.0/images?recursion=1")
		return resp.get("metadata", [])
	except Exception:
		return []


def _filter_images_by_prefix(all_images, prefix):
	"""Filter images that match the given alias prefix."""
	images = []
	for img in all_images:
		aliases = img.get("aliases", [])
		for alias in aliases:
			alias_name = alias.get("name", "")
			if alias_name.startswith(prefix):
				version = alias_name[len(prefix):].lstrip(":")
				images.append(
					{
						"alias": alias_name,
						"version": version or "latest",
						"fingerprint": img.get("fingerprint", ""),
						"created_at": img.get("created_at", ""),
						"size": img.get("size", 0),
						"architecture": img.get("architecture", ""),
					}
				)
	return images


def _format_size(size):
	"""Format byte size to human readable string."""
	if size <= 0:
		return "-"
	if size >= 1024 * 1024 * 1024:
		return f"{size / (1024 * 1024 * 1024):.1f} GB"
	elif size >= 1024 * 1024:
		return f"{size / (1024 * 1024):.1f} MB"
	else:
		return f"{size / 1024:.1f} KB"


@frappe.whitelist()
def get_apps():
	"""Get list of all app templates."""
	_check_admin_permission()
	_ensure_templates_dir()

	meta = _load_meta()

	# Get list of .yaml files in templates directory
	yaml_files = [f for f in os.listdir(TEMPLATES_DIR) if f.endswith(".yaml")]

	# Fetch all images ONCE from image server
	all_images = []
	image_server = _get_image_server()
	if image_server and image_server.get("url"):
		all_images = _get_all_images(image_server["url"])

	apps = []
	for yaml_file in yaml_files:
		app_name = yaml_file.replace(".yaml", "")
		app_meta = meta.get(app_name, {})

		# Filter versions from cached images
		versions = _filter_images_by_prefix(all_images, app_name)
		versions_count = len(versions)
		latest_version = "-"
		total_size = 0

		if versions:
			versions.sort(key=lambda x: x.get("created_at", ""), reverse=True)
			latest_version = versions[0].get("version", "latest")
			total_size = versions[0].get("size", 0)

		apps.append(
			{
				"id": app_name,
				"name": app_name,
				"description": app_meta.get("description", f"Distrobuilder template: {app_name}"),
				"status": app_meta.get("status", "active"),
				"versions_count": versions_count,
				"latest_version": latest_version,
				"currentVersion": latest_version,
				"size": _format_size(total_size),
				"created": app_meta.get("created", ""),
				"lastUpdated": versions[0].get("created_at", "")[:10] if versions else app_meta.get("created", ""),
				"sitesCount": 0,
			}
		)

	return apps


@frappe.whitelist()
def get_app_detail(app_name):
	"""Get app details including YAML config."""
	_check_admin_permission()

	yaml_path = os.path.join(TEMPLATES_DIR, f"{app_name}.yaml")
	if not os.path.exists(yaml_path):
		frappe.throw(f"App template '{app_name}' not found")

	with open(yaml_path, "r") as f:
		yaml_content = f.read()

	meta = _load_meta()
	app_meta = meta.get(app_name, {})

	# Get versions from image server
	image_server = _get_image_server()
	all_images = []
	if image_server and image_server.get("url"):
		all_images = _get_all_images(image_server["url"])

	versions = _filter_images_by_prefix(all_images, app_name)
	versions.sort(key=lambda x: x.get("created_at", ""), reverse=True)

	formatted_versions = []
	for i, v in enumerate(versions):
		formatted_versions.append(
			{
				"version": v.get("version", "latest"),
				"alias": v.get("alias", ""),
				"fingerprint": v.get("fingerprint", "")[:12],
				"released": v.get("created_at", "")[:10],
				"size": _format_size(v.get("size", 0)),
				"status": "current" if i == 0 else "active",
				"sitesCount": 0,
				"changelog": "",
			}
		)

	latest_version = formatted_versions[0].get("version") if formatted_versions else "-"
	size = formatted_versions[0].get("size") if formatted_versions else "-"

	return {
		"id": app_name,
		"name": app_name,
		"description": app_meta.get("description", f"Distrobuilder template: {app_name}"),
		"status": app_meta.get("status", "active"),
		"yaml_content": yaml_content,
		"versions": formatted_versions,
		"currentVersion": latest_version,
		"size": size,
		"sitesCount": 0,
		"lastUpdated": formatted_versions[0].get("released") if formatted_versions else app_meta.get("created", ""),
	}


@frappe.whitelist()
def save_app_config(app_name, yaml_content):
	"""Save YAML configuration for an app."""
	_check_admin_permission()

	yaml_path = os.path.join(TEMPLATES_DIR, f"{app_name}.yaml")
	if not os.path.exists(yaml_path):
		frappe.throw(f"App template '{app_name}' not found")

	try:
		yaml.safe_load(yaml_content)
	except yaml.YAMLError as e:
		frappe.throw(f"Invalid YAML: {str(e)}")

	with open(yaml_path, "w") as f:
		f.write(yaml_content)

	return {"success": True, "message": f"Configuration for '{app_name}' saved successfully"}


@frappe.whitelist()
def create_app(app_name, description, yaml_content):
	"""Create a new app template."""
	_check_admin_permission()
	_ensure_templates_dir()

	if not app_name or not all(c.isalnum() or c in "-_" for c in app_name):
		frappe.throw("App name must be alphanumeric (dashes and underscores allowed)")

	yaml_path = os.path.join(TEMPLATES_DIR, f"{app_name}.yaml")
	if os.path.exists(yaml_path):
		frappe.throw(f"App template '{app_name}' already exists")

	try:
		yaml.safe_load(yaml_content)
	except yaml.YAMLError as e:
		frappe.throw(f"Invalid YAML: {str(e)}")

	with open(yaml_path, "w") as f:
		f.write(yaml_content)

	meta = _load_meta()
	meta[app_name] = {
		"name": app_name,
		"description": description or f"Distrobuilder template: {app_name}",
		"status": "active",
		"created": frappe.utils.today(),
	}
	_save_meta(meta)

	return {"success": True, "message": f"App template '{app_name}' created successfully"}


@frappe.whitelist()
def delete_app(app_name):
	"""Delete an app template."""
	_check_admin_permission()

	yaml_path = os.path.join(TEMPLATES_DIR, f"{app_name}.yaml")
	if not os.path.exists(yaml_path):
		frappe.throw(f"App template '{app_name}' not found")

	os.remove(yaml_path)

	meta = _load_meta()
	if app_name in meta:
		del meta[app_name]
		_save_meta(meta)

	return {"success": True, "message": f"App template '{app_name}' deleted successfully"}


@frappe.whitelist()
def build_image(app_name, version, target_server=None, release="edge"):
	"""Build image using distrobuilder and import to Incus via REST API."""
	_check_admin_permission()

	logs = []
	template_path = os.path.join(TEMPLATES_DIR, f"{app_name}.yaml")

	if not os.path.exists(template_path):
		return {"success": False, "error": f"Template '{app_name}' not found", "logs": []}

	# Determine target server
	if not target_server:
		image_server = _get_image_server()
		if image_server:
			target_server = image_server["name"]
		else:
			return {"success": False, "error": "No image server configured", "logs": []}

	server_url = _get_server_url(target_server)
	if not server_url:
		return {"success": False, "error": f"Server '{target_server}' URL not found", "logs": []}

	if not os.path.exists(CLIENT_CERT) or not os.path.exists(CLIENT_KEY):
		return {"success": False, "error": "Panel certificate not configured", "logs": []}

	build_dir = tempfile.mkdtemp(prefix=f"build-{app_name}-")
	logs.append("=== Build Configuration ===")
	logs.append(f"App: {app_name}")
	logs.append(f"Version: {version or 'latest'}")
	logs.append(f"Release: {release}")
	logs.append(f"Target Server: {target_server}")
	logs.append(f"Build directory: {build_dir}")

	try:
		build_template = os.path.join(build_dir, "template.yaml")
		shutil.copy(template_path, build_template)
		logs.append("Template copied to build directory")

		logs.append("")
		logs.append("=== Running distrobuilder ===")
		logs.append("This may take several minutes...")

		result = subprocess.run(
			[
				"doas",
				"distrobuilder",
				"build-incus",
				"-o", "image.architecture=amd64",
				"-o", f"image.release={release}",
				"template.yaml",
			],
			cwd=build_dir,
			capture_output=True,
			text=True,
			timeout=600,
		)

		if result.stdout:
			for line in result.stdout.strip().split("\n"):
				if line.strip():
					logs.append(line.strip())

		if result.stderr:
			for line in result.stderr.strip().split("\n"):
				if line.strip():
					logs.append(line.strip())

		if result.returncode != 0:
			error_msg = result.stderr or "Unknown error"
			logs.append(f"Build failed")
			return {"success": False, "error": error_msg, "logs": logs}

		logs.append("Build completed successfully")

		# Check for output files
		incus_tar = os.path.join(build_dir, "incus.tar.xz")
		rootfs_squashfs = os.path.join(build_dir, "rootfs.squashfs")

		if not os.path.exists(incus_tar):
			logs.append("Output file not found: incus.tar.xz")
			return {"success": False, "error": "Build output not found", "logs": logs}

		# Import to Incus via REST API
		logs.append("")
		logs.append(f"=== Importing to {target_server} via API ===")

		alias = f"{app_name}:{version}" if version else app_name

		# Prepare multipart upload
		if os.path.exists(rootfs_squashfs):
			logs.append("Importing split image (metadata + rootfs)")
			# For split images, use multipart upload
			with open(incus_tar, "rb") as meta_file, open(rootfs_squashfs, "rb") as rootfs_file:
				headers = {
					"X-Incus-public": "false",
				}
				# Incus expects multipart with metadata.tar.xz and rootfs.squashfs
				files = {
					"metadata": ("incus.tar.xz", meta_file, "application/octet-stream"),
					"rootfs": ("rootfs.squashfs", rootfs_file, "application/octet-stream"),
				}
				r = requests.post(
					f"{server_url}/1.0/images",
					headers=headers,
					files=files,
					cert=(CLIENT_CERT, CLIENT_KEY),
					verify=False,
					timeout=300,
				)
		else:
			logs.append("Importing unified image")
			with open(incus_tar, "rb") as f:
				headers = {
					"Content-Type": "application/octet-stream",
					"X-Incus-public": "false",
				}
				r = requests.post(
					f"{server_url}/1.0/images",
					headers=headers,
					data=f,
					cert=(CLIENT_CERT, CLIENT_KEY),
					verify=False,
					timeout=300,
				)

		if r.status_code not in (200, 202):
			error_msg = r.text
			logs.append(f"Import failed: {error_msg}")
			return {"success": False, "error": error_msg, "logs": logs}

		resp = r.json()

		# Wait for async operation if needed
		if resp.get("type") == "async":
			op_url = resp.get("operation", "")
			if op_url:
				logs.append("Waiting for import operation...")
				wait_r = requests.get(
					f"{server_url}{op_url}/wait",
					cert=(CLIENT_CERT, CLIENT_KEY),
					verify=False,
					timeout=300,
				)
				wait_resp = wait_r.json()
				if wait_resp.get("metadata", {}).get("status") != "Success":
					error = wait_resp.get("metadata", {}).get("err", "Unknown error")
					logs.append(f"Import operation failed: {error}")
					return {"success": False, "error": error, "logs": logs}

		# Get fingerprint from response to set alias
		fingerprint = ""
		op_metadata = resp.get("metadata", {})
		if resp.get("type") == "async":
			# For async, get fingerprint from operation metadata
			if op_url:
				op_r = requests.get(
					f"{server_url}{op_url}",
					cert=(CLIENT_CERT, CLIENT_KEY),
					verify=False,
					timeout=30,
				)
				op_resp = op_r.json()
				op_meta = op_resp.get("metadata", {}).get("metadata", {})
				fingerprint = op_meta.get("fingerprint", "")
		else:
			fingerprint = op_metadata.get("fingerprint", "")

		# Set alias for the image
		if fingerprint:
			try:
				_incus_api(server_url, "POST", "/1.0/images/aliases", {
					"name": alias,
					"target": fingerprint,
				})
				logs.append(f"Alias '{alias}' set for image {fingerprint[:12]}")
			except Exception as e:
				# Alias might already exist, try to update
				try:
					_incus_api(server_url, "PUT", f"/1.0/images/aliases/{alias}", {
						"target": fingerprint,
					})
					logs.append(f"Alias '{alias}' updated for image {fingerprint[:12]}")
				except Exception:
					logs.append(f"Warning: Could not set alias '{alias}': {str(e)}")

		logs.append(f"Image imported as '{alias}'")
		logs.append("")
		logs.append("=== Build completed successfully! ===")

		return {
			"success": True,
			"message": f"Image '{alias}' built and imported successfully",
			"logs": logs,
			"alias": alias,
		}

	except subprocess.TimeoutExpired:
		logs.append("Build timed out (exceeded 10 minutes)")
		return {"success": False, "error": "Build timed out", "logs": logs}

	except Exception as e:
		logs.append(f"Error: {str(e)}")
		frappe.log_error(f"Build failed for {app_name}: {str(e)}", "Image Build Error")
		return {"success": False, "error": str(e), "logs": logs}

	finally:
		try:
			subprocess.run(["doas", "rm", "-rf", build_dir], timeout=10, capture_output=True)
		except Exception:
			shutil.rmtree(build_dir, ignore_errors=True)


@frappe.whitelist()
def get_app_versions(app_name):
	"""Get versions of an app from Incus images."""
	_check_admin_permission()

	image_server = _get_image_server()
	if not image_server or not image_server.get("url"):
		return []

	all_images = _get_all_images(image_server["url"])
	versions = _filter_images_by_prefix(all_images, app_name)
	versions.sort(key=lambda x: x.get("created_at", ""), reverse=True)

	formatted = []
	for v in versions:
		formatted.append(
			{
				"version": v.get("version", "latest"),
				"alias": v.get("alias", ""),
				"fingerprint": v.get("fingerprint", ""),
				"created_at": v.get("created_at", ""),
				"size": _format_size(v.get("size", 0)),
			}
		)

	return formatted


@frappe.whitelist()
def delete_image(fingerprint, target_server=None):
	"""Delete an image from Incus."""
	_check_admin_permission()

	if not target_server:
		image_server = _get_image_server()
		if image_server:
			target_server = image_server["name"]
		else:
			return {"success": False, "error": "No image server configured"}

	server_url = _get_server_url(target_server)
	if not server_url:
		return {"success": False, "error": f"Server '{target_server}' URL not found"}

	try:
		_incus_api(server_url, "DELETE", f"/1.0/images/{fingerprint}")
		return {"success": True, "message": "Image deleted successfully"}
	except Exception as e:
		return {"success": False, "error": str(e)}


@frappe.whitelist()
def update_app_meta(app_name, description=None, status=None):
	"""Update app metadata."""
	_check_admin_permission()

	yaml_path = os.path.join(TEMPLATES_DIR, f"{app_name}.yaml")
	if not os.path.exists(yaml_path):
		frappe.throw(f"App template '{app_name}' not found")

	meta = _load_meta()

	if app_name not in meta:
		meta[app_name] = {
			"name": app_name,
			"created": frappe.utils.today(),
		}

	if description is not None:
		meta[app_name]["description"] = description

	if status is not None:
		meta[app_name]["status"] = status

	_save_meta(meta)

	return {"success": True, "message": f"Metadata for '{app_name}' updated successfully"}
