_H='Timeout exceed for the pod load operation. To avoid this issue, try to increase thevalue of the POD_LOAD_CLI_TIMEOUT configuration variable.'
_G='merge_strategy'
_F='pod_version'
_E='services'
_D='pod_name'
_C=False
_B=True
_A=None
import io,json,logging,os,zipfile
from collections import defaultdict
from dataclasses import dataclass,field
from enum import Enum
from functools import singledispatch
from pathlib import Path
from typing import Callable,Dict,List,NamedTuple,Optional,Set,Tuple
from urllib.parse import urlparse
import requests,yaml
from localstack import config
from localstack.cli import console
from localstack.utils.files import load_file,new_tmp_file,rm_rf,save_file
from localstack.utils.http import download,safe_requests
from localstack.utils.strings import to_str
from localstack_persistence.constants import ASSETS_DIRECTORY
from localstack_ext.bootstrap import licensing
from localstack_ext.bootstrap.pods.api_types import DEFAULT_MERGE_STRATEGY,MergeStrategy,StateMergeRequest
from localstack_ext.bootstrap.pods.client_api import CloudPodsClientApi
from localstack_ext.bootstrap.pods.models import Version
from localstack_ext.bootstrap.pods.remote import CloudPodsRemote,CloudPodsRemotePlatform,RemoteConfig
from localstack_ext.bootstrap.pods.service_state.service_state import ServiceState
from localstack_ext.bootstrap.pods.utils.adapters import ServiceStateMarshaller
from localstack_ext.bootstrap.pods.utils.common import PodsConfigContext
from localstack_ext.bootstrap.pods.utils.persistence import marshall_object
from localstack_ext.config import POD_LOAD_CLI_TIMEOUT
from localstack_ext.constants import API_PATH_PODS,API_STATES_DIR,CLOUDPODS_METADATA_FILE
LOG=logging.getLogger(__name__)
class PodInfo:
	def __init__(A,name=_A,pod_size=0):A.name=name;A.pod_size=pod_size;A.pod_size_compressed=0;A.persisted_resource_names=[]
class PodLocation(Enum):REMOTE='remote';LOCAL='local'
@dataclass
class PodMetadata:services:List[str]=field(default_factory=list);size:int=0
def get_state_zip_from_instance(services=_A):
	B=services;C=f"{get_pods_endpoint()}/state";D=','.join(B)if B else'';A=requests.get(C,params={_E:D});E=PodMetadata(services=A.headers.get('x-localstack-pod-services','').split(','),size=int(A.headers.get('x-localstack-pod-size',0)))
	if A.status_code>=400:raise Exception(f"Unable to get local pod state via management API {C} (code {A.status_code}): {A.content}")
	return A.content,E
class CloudPodsManager:
	pod_name:str;remote_config:Optional[RemoteConfig]
	def __init__(A,pod_name,remote_config=_A):B=pod_name;A.pod_name=B;A.remote_config=remote_config;C=PodsConfigContext(pod_name=B);A.pods_api=CloudPodsClientApi(C)
	def init(A):A.pods_api.init(pod_name=A.pod_name)
	def _add_state_to_cloud_pods_store(A,extract_assets=_C,services=_A):
		if not A.pods_api.config_context.is_initialized():LOG.debug('No Cloud Pod instance detected in the local context - unable to add state');return
		E,L=get_state_zip_from_instance(services=services);C=ServiceStateMarshaller.unmarshall(E,raw_bytes=_B)
		for (B,F) in C.state.items():
			for (G,H) in F.backends.items():A.pods_api.create_state_file_from_fs(file_name=G,service=B.service,region=B.region,root=API_STATES_DIR,account_id=B.account_id,object=H)
		if extract_assets:
			for (I,J) in C.assets.items():
				for (D,K) in J.items():A.pods_api.create_state_file_from_fs(rel_path=D,file_name=os.path.basename(D),service=I,region='NA',root=ASSETS_DIRECTORY,account_id='NA',object=K)
	def delete(A,remote):
		C=A.pods_api.config_context.cloud_pods_root_dir;B=os.path.join(C,A.pod_name)
		if os.path.isdir(B):rm_rf(B);return _B
		if remote:return A.remote.delete_pod(A.pod_name)
		return _C
	def push(A,comment=_A,services=_A):
		A.pods_api.set_pod_context(A.pod_name);B=A.pods_api.get_max_version();C=A.remote.is_registered(A.pod_name)
		if C:
			E=A.remote.get_max_version(pod_name=A.pod_name);F=not B or B.version_number<E
			if F:A.pull(inject=_C);B=A.pods_api.get_max_version()
		A._add_state_to_cloud_pods_store(extract_assets=_B,services=services);D=A.pods_api.push(comment=comment)
		if C:A.remote.push_pod(A.pod_name,version=D.version_number)
		LOG.debug("Created new version %s for pod '%s'",D.version_number,A.pod_name);return PodInfo()
	def push_overwrite(A,version,comment=_A,services=_A):
		B=version;A.pods_api.set_pod_context(pod_name=A.pod_name);C=A.pods_api.get_max_version_no()
		if C and B>C:LOG.warning(f"Version {B} does not exist");return _C
		A._add_state_to_cloud_pods_store(services=services);A.pods_api.push_overwrite(version=B,comment=comment)
		if A.remote.is_registered(A.pod_name):A.remote.push_pod(A.pod_name,version=B,overwrite=_B)
		return _B
	def pull(A,inject=_B,merge_strategy=_A):
		C=merge_strategy
		if C is _A:C=DEFAULT_MERGE_STRATEGY
		D=0;B=A.pod_name
		if B in A.pods_api.list_locally_available_pods():A.pods_api.set_pod_context(B);D=A.pods_api.get_max_version_no()
		else:A.pods_api.init(pod_name=B)
		E=A.remote.get_max_version(B)
		if not E:return _C
		if E>D:F=list(range(D+1,E+1));A.remote.pull_versions(pod_name=B,required_versions=F)
		else:LOG.info('No new version available remotely')
		if inject:LOG.info('Injecting the cloud pod state into the running instance');return A.inject(version=-1,merge_strategy=C)
		return _B
	def commit(A,message=_A,services=_A):A.pods_api.set_pod_context(A.pod_name);A._add_state_to_cloud_pods_store(services=services);B=A.pods_api.commit(message=message);LOG.debug('Completed revision: %s',B.hash_ref)
	@staticmethod
	def deploy_pod_into_instance(pod_name,pod_version,pod_path,merge_strategy):
		A=pod_path
		if not A:raise Exception(f"Unable to restore pod state via local pods management API: Pod Path {A} not valid")
		try:
			D=load_file(A,mode='rb');C=get_pods_endpoint();E={_D:pod_name,_F:pod_version,_G:merge_strategy}
			try:B=requests.post(C,data=D,params=E,timeout=POD_LOAD_CLI_TIMEOUT)
			except requests.exceptions.Timeout as F:raise Exception(_H) from F
			if B.status_code>=400:raise Exception(f"Unable to restore pod state via local pods management API {C} (code {B.status_code}): {B.content}")
		except requests.exceptions.ReadTimeout:LOG.debug('Returning from cloud pod inject operation - continues to run in the background...')
	def inject(A,version=-1,merge_strategy=_A):
		D=merge_strategy;C=version
		if D is _A:D=MergeStrategy.MERGE
		if not A.pods_api.config_context.pod_exists_locally(A.pod_name):raise Exception('Unable to find a local version of pod %s',A.pod_name)
		A.pods_api.set_pod_context(A.pod_name)
		if C==-1:C=A.pods_api.get_max_version_no()
		B=A.pods_api.config_context.get_version_state_archive();E=_C
		try:
			if not B and A.remote.is_registered(A.pod_name):raise Exception(f"Unable to find a local version of pod {A.pod_name}")
			if not B and C<=Version.DEFAULT_INITIAL_VERSION_NUMBER:B=new_tmp_file();F=A.pods_api.get_state_archive_from_state_files();save_file(B,F);E=_B
			if not B:raise Exception(f"Unable to find state archive for cloud pod '{A.pod_name}' version {C}")
			A.deploy_pod_into_instance(pod_name=A.pods_api.config_context.pod_name,pod_version=C,pod_path=B,merge_strategy=D);return _B
		except Exception as G:LOG.debug('An exception occurred while trying to load Cloud Pod %s: "%s"',A.pod_name,G);return _C
		finally:
			if E:rm_rf(B)
	def get_version_summaries(A):A.pods_api.set_pod_context(A.pod_name);B=A.pods_api.get_version_summaries();return B
	def version_metamodel(A,version):
		B=version;A.pods_api.set_pod_context(A.pod_name)
		if B==-1:B=A.pods_api.get_max_version_no()
		D=A.pods_api.get_version_by_number(B);E=D.get_latest_revision(with_commit=_B);C=A.pods_api.commit_metamodel_utils.reconstruct_metamodel(version=D,revision=E)
		if not C and A.remote.is_registered(A.pod_name):A.remote.pull_versions(A.pod_name,required_versions=[B]);C=A.pods_api.commit_metamodel_utils.create_metamodel_from_state_files(version=B)
		return C
	def set_version(A,version,inject_version_state,reset_state,commit_before):
		B=version;A.pods_api.set_pod_context(A.pod_name);C=A.pods_api.set_active_version(version_no=B,commit_before=commit_before)
		if not C:LOG.warning(f"Could not find version {B}")
		if inject_version_state:A.inject(version=B,merge_strategy=MergeStrategy.OVERWRITE)
		return C
	def list_version_commits(A,version):A.pods_api.set_pod_context(A.pod_name);B=A.pods_api.list_version_commits(version_no=version);C=[A.get_summary()for A in B];return C
	def get_commit_diff(A,version,commit):A.pods_api.set_pod_context(A.pod_name);B=A.pods_api.commit_metamodel_utils.get_commit_diff(version_no=version,commit_no=commit);return B
	def rename_pod(A,current_pod_name,new_pod_name):
		C=current_pod_name;B=new_pod_name;A.pods_api.set_pod_context(C)
		if B in A.pods_api.list_locally_available_pods():LOG.warning(f"{B} already exists locally");return _C
		if A.remote.is_registered(C):
			D=A.remote.rename_pod(C,B)
			if not D:return D
		A.pods_api.rename_pod(B);return _B
	def register_remote(A,ci_pod=_A):return A.remote.register_remote(A.pod_name,ci_pod=ci_pod)
	def list_pods(B):
		D={A:{PodLocation.LOCAL.value}for A in B.pods_api.list_locally_available_pods()};C=D or defaultdict(set);E=B.remote.list_pods()
		for A in E or[]:F=A.get(_D)if isinstance(A,dict)else A;C.setdefault(F,set()).add(PodLocation.REMOTE.value)
		return C
	@property
	def remote(self):
		A=self;C=''
		if A.remote_config and A.remote_config.remote_url:C=A.remote_config.remote_url
		B=urlparse(C).scheme
		if not B:B=list(CloudPodsRemotePlatform.protocols())[0]
		return CloudPodsRemote.get_for_protocol(B,config_context=A.pods_api.config_context,remote_config=A.remote_config)
class PodConfigManagerMeta(type):
	def __getattr__(C,attr):
		def A(*D,**E):
			A=_A
			for F in C.CHAIN:
				try:
					B=getattr(F,attr)(*(D),**E)
					if B:
						if not A:A=B
						elif isinstance(B,list)and isinstance(A,list):A.extend(B)
				except Exception:
					if LOG.isEnabledFor(logging.DEBUG):LOG.exception('error during PodConfigManager call chain')
			if A is not _A:return A
			raise Exception(f'Unable to run operation "{attr}" for local or remote configuration')
		return A
class PodConfigManager(metaclass=PodConfigManagerMeta):
	CHAIN=[]
	@classmethod
	def pod_config(D,pod_name):
		A=pod_name;C=PodConfigManager.list_pods();B=[B for B in C if B[_D]==A]
		if not B:raise Exception(f'Unable to find config for pod named "{A}"')
		return B[0]
def get_pods_manager(pods_name,remote_config=_A):return CloudPodsManager(pod_name=pods_name,remote_config=remote_config)
def init_cloudpod(pod_name):A=get_pods_manager(pods_name=pod_name);A.init()
def delete_pod(pod_name):A=get_pods_manager(pods_name=pod_name);B=A.delete(remote=_B);return B
def register_remote(pod_name,ci_pod=_A):A=get_pods_manager(pods_name=pod_name);B=A.register_remote(ci_pod=ci_pod);return B
def rename_pod(current_pod_name,new_pod_name):A=new_pod_name;B=get_pods_manager(pods_name=A);C=B.rename_pod(current_pod_name=current_pod_name,new_pod_name=A);return C
def list_pods():A=get_pods_manager(pods_name='');B=A.list_pods();return B
def list_public_pods():
	B=CloudPodsRemotePlatform.create_platform_url('public');C=licensing.get_auth_headers();A=safe_requests.get(B,headers=C)
	if not A.ok:raise Exception(to_str(A.content))
	D=json.loads(A.content);return[A[_D]for A in D]
def set_public(pod_name,public):
	A=pod_name;B=get_pods_manager(A);B.pods_api.set_pod_context(A);C=CloudPodsRemotePlatform(B.pods_api.config_context)
	if not C.is_registered(A):return _C
	D=CloudPodsRemotePlatform.create_platform_url(A);E=licensing.get_auth_headers();F=safe_requests.patch(D,headers=E,data=json.dumps({'is_public':public}));return F.ok
def commit_state(pod_name,message=_A,services=_A,**C):
	B=pod_name;A=get_pods_manager(pods_name=B)
	if not A.pods_api.config_context.is_initialized():A.init()
	A.pods_api.set_pod_context(pod_name=B);A.commit(message=message,services=services)
def inject_state(pod_name,version,merge_strategy=_A,**C):A=get_pods_manager(pods_name=pod_name);B=A.inject(version=version,merge_strategy=merge_strategy);return B
def get_version_summaries(pod_name):B=get_pods_manager(pods_name=pod_name);A=B.get_version_summaries();A=A[::-1];return A
def get_version_metamodel(version,pod_name,**C):A=get_pods_manager(pods_name=pod_name);B=A.version_metamodel(version=version);return B
def set_version(version,inject_version_state,reset_state,commit_before,pod_name,**C):A=get_pods_manager(pods_name=pod_name);B=A.set_version(version=version,inject_version_state=inject_version_state,reset_state=reset_state,commit_before=commit_before);return B
def list_version_commits(version,pod_name):A=get_pods_manager(pods_name=pod_name);B=A.list_version_commits(version=version);return B
def get_commit_diff(version,commit,pod_name):A=get_pods_manager(pods_name=pod_name);B=A.get_commit_diff(version=version,commit=commit);return B
def push_overwrite(version,pod_name,comment,services=_A):A=get_pods_manager(pods_name=pod_name);A.push_overwrite(version=version,comment=comment,services=services)
def push_state(pod_name,remote_config=_A,comment=_A,services=_A,local=_C):
	C=pod_name;B=services
	if B is _A:B=[]
	A=get_pods_manager(pods_name=C,remote_config=remote_config)
	if not A.pods_api.config_context.is_initialized():A.init()
	A.pods_api.set_pod_context(pod_name=C);A.push(comment=comment,services=B)
	if local:return _B
	return A.register_remote()
def get_pods_endpoint():A=config.get_edge_url();return f"{A}{API_PATH_PODS}"
def pull_state(pod_name,remote_config=_A,merge_strategy=_A):
	A=pod_name
	if not A:raise Exception('Need to specify a cloud pod name')
	B=get_pods_manager(pods_name=A,remote_config=remote_config);return B.pull(inject=_B,merge_strategy=merge_strategy)
def export_pod(target):
	C=target;F,G=get_state_zip_from_instance();D=urlparse(C);A=os.path.abspath(os.path.join(D.netloc,D.path));E=Path(A).parent.absolute()
	if not os.path.exists(E):raise Exception(f"{E} is not a valid path")
	save_file(file=A,content=F);B=get_environment_metadata();B['name']=os.path.basename(C);B.update(G.__dict__)
	with zipfile.ZipFile(file=A,mode='a')as H:H.writestr(CLOUDPODS_METADATA_FILE,yaml.dump(B))
	return _B
def import_pod(source):
	C='pro';A=source;D=get_protocol_access(A);B=D(A);E=zipfile.ZipFile(io.BytesIO(B),'r');F=read_metadata_from_pod(E)or{};G=get_environment_metadata().get(C)
	if F.get(C,_C)and not G:console.print('Warning: You are trying to load a Cloud Pod generated with a Pro license.The loaded state might be incomplete.')
	return inject_pod_endpoint(content=B)
@singledispatch
def read_metadata_from_pod(zip_file):
	try:A=yaml.safe_load(zip_file.read(CLOUDPODS_METADATA_FILE));return A
	except KeyError:LOG.debug('No %s file in the archive',CLOUDPODS_METADATA_FILE)
@read_metadata_from_pod.register(bytes)
def _(zip_file):A=zip_file;A=zipfile.ZipFile(io.BytesIO(A),'r');return read_metadata_from_pod(A)
@read_metadata_from_pod.register(str)
def _(zip_file):
	with zipfile.ZipFile(zip_file)as A:return read_metadata_from_pod(A)
def inject_pod_endpoint(content,pod_name='community_pod',pod_version=1,merge_strategy=DEFAULT_MERGE_STRATEGY):
	A=get_pods_endpoint();B={_D:pod_name,_F:str(pod_version),_G:merge_strategy}
	try:C=requests.post(A,data=content,params=B,timeout=POD_LOAD_CLI_TIMEOUT)
	except requests.exceptions.Timeout as D:raise Exception(_H) from D
	return C.ok
def get_environment_metadata():
	C=get_pods_endpoint();A=f"{C}/environment";B=requests.get(A)
	if not B.ok:raise Exception(f"Unable to retrieve environment metadata from {A}")
	return json.loads(B.content)
def get_git_base_url(user,repo):return f"https://raw.githubusercontent.com/{user}/{repo}/main"
def reset_local_state(reset_persistence=_C,services=_A):
	B=f"{config.get_edge_url()}/_localstack/state/reset";C={'persistence':reset_persistence,_E:services or[]};LOG.debug(f"Sending request to reset the service states in local instance with params {json.dumps(C)}.");A=requests.post(B)
	if not A.ok:raise Exception(f"Unable to reset service state via local management API {B} (code {A.status_code}): {A.content}")
class States(NamedTuple):state:Optional[ServiceState];inject:ServiceState;ancestor:Optional[ServiceState]
def _merge_states_into_zip(states):
	A=io.BytesIO()
	with zipfile.ZipFile(A,'a')as C:
		for (D,B) in states._asdict().items():
			if not B:continue
			C.writestr(f"{D}.zip",ServiceStateMarshaller.marshall(B))
	A.seek(0);return A.getvalue()
def merge_states_via_endpoint(merge_strategy,injecting,state=_A,ancestor=_A):
	C=_merge_states_into_zip(States(state,injecting,ancestor));D=StateMergeRequest(merge_strategy=merge_strategy,data=C);B=f"{get_pods_endpoint()}/merge";E=marshall_object(D);A=requests.post(url=B,data=E)
	if A.status_code>=400:raise Exception(f"Unable to merge state via merge API {B} (code {A.status_code})")
	return A.content
def merge_local_state_with(merge_strategy,state_archive_path):
	B=state_archive_path;A=merge_strategy
	if A==MergeStrategy.OVERWRITE:reset_local_state(reset_persistence=_B);return B
	D=ServiceStateMarshaller.unmarshall_zip_archive(B);E=merge_states_via_endpoint(merge_strategy=A,injecting=D);C=new_tmp_file();ServiceStateMarshaller.marshall_zip_archive(C,ServiceStateMarshaller.unmarshall(E,raw_bytes=_B));return C
def is_initialized(pod_name):A=get_pods_manager(pods_name=pod_name);return A.pods_api.config_context.is_initialized()
def get_protocol_access(url):
	A=urlparse(url).scheme
	if A=='file':return get_zip_content_from_file
	elif A in['http','https']:return get_zip_content_from_http
	elif A=='git':return get_zip_content_from_git
	raise Exception(f"Protocol {A} not valid")
def get_zip_content_from_file(url):
	B=url;B=urlparse(B);A=os.path.abspath(os.path.join(B.netloc,B.path))
	if not os.path.isfile(A):raise Exception(f"Path {A} is not a file")
	if not os.path.exists(A):raise Exception(f"Path {A} does not exist")
	return load_file(A,mode='rb')
def get_zip_content_from_http(url):
	A=requests.get(url)
	if not A.ok:raise Exception('Unable to fetch Cloud Pod from URL %s: %s %s',url,A.status_code,A.content)
	return A.content
def get_zip_content_from_git(url):
	E=url[len('git://'):];A=E.split('/');F,G,H=A[0],A[1],A[2];I=f"{get_git_base_url(F,G)}";C=f"{I}/{H}";B=new_tmp_file()
	try:download(C,B);return load_file(B,mode='rb')
	except Exception as D:raise Exception(f"Failed to download Cloud Pod from URL {C}: {D}") from D
	finally:rm_rf(B)
def get_persisted_resource_names(data_dir):
	E='api_states';D=data_dir;B=[]
	with os.scandir(D)as C:
		for A in C:
			if A.is_dir()and A.name!=E:B.append(A.name)
	with os.scandir(os.path.join(D,E))as C:
		for A in C:
			if A.is_dir()and len(os.listdir(A.path))>0:B.append(A.name)
	LOG.debug(f"Detected state files for the following APIs: {B}");return B
PODS_NAMESPACE_DELIM='-'