from __future__ import annotations
_A=None
import logging,os,sqlite3
from shutil import copyfile
from typing import Any,Dict,List,Optional,Set,Tuple
from localstack.utils.files import mkdir
from localstack.utils.strings import long_uid
from localstack_ext.bootstrap.state_merge import merge_3way
LOG=logging.getLogger(__name__)
class DDBAttribute:
	_PRIMARY_KEY='PRIMARY KEY';_NOT_NULLABLE='NOT NULL';_DEFAULT='DEFAULT';_NULL='NULL';_TEXT_TYPES={'TEXT'};_NUMERIC_TYPES={'NUMERIC','BOOLEAN','DATE','DATETIME'};_INTEGER_TYPES={'INT','INTEGER'};_REAL_TYPES={'REAL','DOUBLE','FLOAT'};_BLOB_TYPES={'BLOB'}
	def __init__(A,name,typ,nullable,default,primary):A.name=name;A.typ=typ;A.nullable=nullable;A.default=default;A.primary=primary
	@classmethod
	def from_table_info(B,table_info):A=table_info;C=str(A[1]);D=str(A[2]);E=not bool(A[3]);F=A[4];G=bool(A[5]);return B(C,D,E,F,G)
	def get_schema(A):
		B=[A.name,A.typ]
		if A.primary:B.append(A._PRIMARY_KEY)
		if not A.nullable:B.append(A._NOT_NULLABLE)
		if A.default:B.extend([A._DEFAULT,A.default])
		return ' '.join(B).strip()
	def __str__(A):return A.get_schema()
	def _default_of_typ(A,typ_name):
		B=typ_name
		if B in A._TEXT_TYPES:return str()
		elif B in A._BLOB_TYPES:return bytes()
		elif B in{*A._NUMERIC_TYPES,*A._INTEGER_TYPES}:return int()
		elif B in A._REAL_TYPES:return float()
		else:LOG.warning(f"No type affinities found for type {B}");return _A
	def _try_cast_to_type_value(C,typ,value,def_value):
		B=def_value;A=value
		if A:
			try:
				if isinstance(A,str)and isinstance(B,bytes):return A
				else:return typ(A)
			except Exception as D:LOG.warning(f"Cannot update value type of '{A}' '{typ}' to be a {C.typ}, hence replaced it with default {B}, error: {D}")
		return B
	def _to_compatible_type_value(A,value):
		B=value
		if not B and A.nullable:return B
		if not B and A.default:return A.default
		C=A._default_of_typ(A.typ);D=type(C);E=type(B)
		if D==E:return B
		else:return A._try_cast_to_type_value(D,B,C)
	def adapt_value(A,value):
		B=A._to_compatible_type_value(value)
		if B:return B
		if A.default:return A.default
		if not A.nullable and not A.default:return A._default_of_typ(A.typ)
		return A._NULL
class DDBTableSchema:
	def __init__(A,table_name,attributes_by_name,og_schema):A.table_name=table_name;A._attributes_by_name=attributes_by_name;A._og_schema=og_schema
	@classmethod
	def from_database(B,table_name,cur):A=table_name;C=DDBTableSchema._load_fields(A,cur);D=DDBTableSchema._load_og_schema(A,cur);return B(A,C,D)
	@staticmethod
	def _sqlite_table_info(table_name):return f'PRAGMA table_info("{table_name}");'
	@staticmethod
	def _sqlite_table_schema(table_name):return f'SELECT sql FROM sqlite_master WHERE tbl_name="{table_name}" LIMIT 1;'
	@staticmethod
	def _load_fields(table_name,cur):
		C=DDBTableSchema._sqlite_table_info(table_name);cur.execute(C);D=cur.fetchall();A={}
		for E in D:B=DDBAttribute.from_table_info(E);A[B.name]=B
		return A
	@staticmethod
	def _load_og_schema(table_name,cur):A=DDBTableSchema._sqlite_table_schema(table_name);cur.execute(A);B=cur.fetchall();C=B[0][0];return C
	def get_attributes(A):return A._attributes_by_name
	def get_attribute_names(B):
		A=[]
		for C in B._attributes_by_name.values():A.append(C.name)
		return A
	def get_schema(A):B=f'CREATE TABLE "{A.table_name}"';C=','.join([B.get_schema()for B in A._attributes_by_name.values()]);D=f"{B} ({C});";return D
	def get_insert_statement(A):B=','.join(A._attributes_by_name.keys());C=','.join(['?']*len(A._attributes_by_name));D=f'INSERT INTO "{A.table_name}" ({B}) VALUES ({C});';return D
	def __str__(A):return A.get_schema()
class DDBField:
	def __init__(A,value):A.value=value
class DDBRecord:
	def __init__(A,schema,record):B=schema;A.fields_by_name=A._load_fields_by_name(B,record);A.primary_id=A._derive_primary_id(B,A.fields_by_name)
	@staticmethod
	def _derive_primary_id(schema,field_by_name):A=schema.get_attributes().values();B=[B.name for B in A if B.primary]or[B.name for B in A];C=[str(field_by_name[A].value)for A in B];D='-'.join(C);return D
	@staticmethod
	def _load_fields_by_name(schema,record):
		A={};B=schema.get_attributes()
		for (C,D) in enumerate(B.keys()):E=record[C];F=DDBField(E);A[D]=F
		return A
	def get_insert_tuple(C,schema):
		A=[];D=schema.get_attributes()
		for (E,F) in D.items():B=C.fields_by_name.get(E,_A);G=B.value if B else _A;H=F.adapt_value(G);A.append(H)
		return tuple(A)
class DDBTable:
	def __init__(A,table_name,schema):A._table_name=table_name;A._schema=schema;A._records_by_primary_id={}
	@classmethod
	def from_database(B,table_name,cur):A=table_name;C=DDBTableSchema.from_database(A,cur);return B(A,C)
	@staticmethod
	def _sql_select_all(table_name):return f'SELECT * FROM "{table_name}";'
	def offload_records(A):A._records_by_primary_id.clear()
	def load_records(A,cur):
		A.offload_records();C=A._sql_select_all(A._table_name);cur.execute(C);D=cur.fetchall()
		for E in D:B=DDBRecord(A._schema,E);A._records_by_primary_id[B.primary_id]=B
	def get_table_name(A):return A._table_name
	def set_table_name(A,table_name):B=table_name;A._table_name=B;A._schema.table_name=B
	def get_table_schema(A):return A._schema
	def dump_records(A,cur):
		if not A._records_by_primary_id:return
		C=A._schema.get_insert_statement()
		for B in A._records_by_primary_id.values():
			D=B.get_insert_tuple(A._schema)
			try:cur.execute(C,D)
			except Exception as E:LOG.warning(f"Could not insert record {B} after merge: {E}")
class DDBDatabase:
	_SQLITE_GET_TABLES="SELECT name FROM sqlite_master WHERE type='table';"
	def __init__(A,file_path):A._con=sqlite3.connect(file_path);A._cur=A._con.cursor();A._tables_by_name=A._find_tables()
	def _find_tables(A):
		A._cur.execute(A._SQLITE_GET_TABLES);D=A._cur.fetchall();B={}
		for E in D:C=E[0];B[C]=DDBTable.from_database(C,A._cur)
		return B
	def close(A):A._cur.close();A._con.close()
	def commit(A):A._con.commit()
	def commit_and_close(A):A.commit();A.close()
	@staticmethod
	def _generate_table_tmp_name(table):return f"{table.get_table_name()}_{long_uid().replace('-','_')}"
	@staticmethod
	def _sql_delete_table(table_name):return f'DROP TABLE IF EXISTS "{table_name}";'
	@staticmethod
	def _sql_rename_table(table_name,new_table_name):return f'ALTER TABLE "{table_name}" RENAME TO "{new_table_name}";'
	@staticmethod
	def _sql_pragma_enable(enabled):A='TRUE'if enabled else'FALSE';return f"PRAGMA ignore_check_constraints={A};"
	def _pragma_enable(A):B=A._sql_pragma_enable(False);A._cur.execute(B)
	def _pragma_disable(A):B=A._sql_pragma_enable(True);A._cur.execute(B)
	def _create_table(A,table):B=table.get_table_schema().get_schema();A._cur.execute(B)
	def _delete_table(A,table_name):B=A._sql_delete_table(table_name);A._cur.execute(B)
	def _rename_table(A,table_name,new_table_name):B=A._sql_rename_table(table_name,new_table_name);A._cur.execute(B)
	def load_records(A):
		for B in A._tables_by_name.values():B.load_records(A._cur)
	def offload_records(A):
		for B in A._tables_by_name.values():B.offload_records()
	def merge(A,inject,ancestor):
		C=inject;B=ancestor;A.load_records();C.load_records()
		if B:B.load_records()
		E={*A._tables_by_name.keys()};F=C._tables_by_name;G=B._tables_by_name if B else{};merge_3way(A._tables_by_name,F,G);A._pragma_disable()
		for H in E:A._delete_table(H)
		for D in A._tables_by_name.values():A._create_table(D);D.dump_records(A._cur)
		A._pragma_enable()
def merge_dynamodb(path_current,path_inject,path_ancestor=_A):
	C=path_inject;B=path_current;from localstack_ext.services.dynamodb.provider import restart_dynamodb as F;mkdir(B);G=os.listdir(B)
	for A in os.listdir(C):
		D=os.path.join(B,A);E=os.path.join(C,A)
		if A in G:merge_sqlite_dbs(D,E)
		else:copyfile(E,D);LOG.debug('Copied state from previously non-existing region file %s',A)
	F()
def merge_sqlite_dbs(sqlite_file_current,sqlite_file_inject,sqlite_file_ancestor=_A):
	D=sqlite_file_inject;C=sqlite_file_current;A=sqlite_file_ancestor;E=DDBDatabase(C);F=DDBDatabase(D);B=DDBDatabase(A)if A else _A;E.merge(F,B);E.commit_and_close();F.close()
	if B:B.close()
	LOG.debug(f"Successfully merged db {C} with {D} and {A}")