import logging
from typing import Any
from typing import Dict
from typing import Generator
from typing import List
from typing import Tuple

import neo4j
from azure.core.exceptions import ClientAuthenticationError
from azure.core.exceptions import HttpResponseError
from azure.core.exceptions import ResourceNotFoundError
from azure.mgmt.sql import SqlManagementClient
from azure.mgmt.sql.models import SecurityAlertPolicyName
from azure.mgmt.sql.models import TransparentDataEncryptionName
from msrestazure.azure_exceptions import CloudError

from .util.credentials import Credentials
from cartography.util import run_cleanup_job
from cartography.util import timeit

logger = logging.getLogger(__name__)


@timeit
def get_client(credentials: Credentials, subscription_id: str) -> SqlManagementClient:
    """
    Getting the Azure SQL client
    """
    client = SqlManagementClient(credentials, subscription_id)
    return client


@timeit
def get_server_list(credentials: Credentials, subscription_id: str) -> List[Dict]:
    """
    Returning the list of Azure SQL servers.
    """
    try:
        client = get_client(credentials, subscription_id)
        server_list = list(map(lambda x: x.as_dict(), client.servers.list()))

    # ClientAuthenticationError and ResourceNotFoundError are subclasses under HttpResponseError
    except ClientAuthenticationError as e:
        logger.warning(f"Client Authentication Error while retrieving servers - {e}")
        return []
    except ResourceNotFoundError as e:
        logger.warning(f"Server resource not found error - {e}")
        return []
    except HttpResponseError as e:
        logger.warning(f"Error while retrieving servers - {e}")
        return []

    for server in server_list:
        x = server['id'].split('/')
        server['resourceGroup'] = x[x.index('resourceGroups') + 1]

    return server_list


@timeit
def load_server_data(
        neo4j_session: neo4j.Session, subscription_id: str, server_list: List[Dict],
        azure_update_tag: int,
) -> None:
    """
    Ingest the server details into neo4j.
    """
    ingest_server = """
    UNWIND {server_list} as server
    MERGE (s:AzureSQLServer{id: server.id})
    ON CREATE SET s.firstseen = timestamp(),
    s.resourcegroup = server.resourceGroup, s.location = server.location
    SET s.lastupdated = {azure_update_tag},
    s.name = server.name,
    s.kind = server.kind,
    s.state = server.state,
    s.version = server.version
    WITH s
    MATCH (owner:AzureSubscription{id: {AZURE_SUBSCRIPTION_ID}})
    MERGE (owner)-[r:RESOURCE]->(s)
    ON CREATE SET r.firstseen = timestamp()
    SET r.lastupdated = {azure_update_tag}
    """

    neo4j_session.run(
        ingest_server,
        server_list=server_list,
        AZURE_SUBSCRIPTION_ID=subscription_id,
        azure_update_tag=azure_update_tag,
    )


@timeit
def sync_server_details(
        neo4j_session: neo4j.Session, credentials: Credentials, subscription_id: str,
        server_list: List[Dict], sync_tag: int,
) -> None:
    details = get_server_details(credentials, subscription_id, server_list)
    load_server_details(neo4j_session, credentials, subscription_id, details, sync_tag)


@timeit
def get_server_details(
        credentials: Credentials, subscription_id: str, server_list: List[Dict],
) -> Generator[Any, Any, Any]:
    """
    Iterate over each servers to get its resource details.
    """
    for server in server_list:
        dns_alias = get_dns_aliases(credentials, subscription_id, server)
        ad_admins = get_ad_admins(credentials, subscription_id, server)
        r_databases = get_recoverable_databases(credentials, subscription_id, server)
        rd_databases = get_restorable_dropped_databases(credentials, subscription_id, server)
        fgs = get_failover_groups(credentials, subscription_id, server)
        elastic_pools = get_elastic_pools(credentials, subscription_id, server)
        databases = get_databases(credentials, subscription_id, server)
        yield server['id'], server['name'], server[
            'resourceGroup'
        ], dns_alias, ad_admins, r_databases, rd_databases, fgs, elastic_pools, databases


@timeit
def get_dns_aliases(credentials: Credentials, subscription_id: str, server: Dict) -> List[Dict]:
    """
    Returns details of the DNS aliases in a server.
    """
    try:
        client = get_client(credentials, subscription_id)
        dns_aliases = list(
            map(
                lambda x: x.as_dict(),
                client.server_dns_aliases.list_by_server(server['resourceGroup'], server['name']),
            ),
        )

    except ClientAuthenticationError as e:
        logger.warning(f"Client Authentication Error while retrieving DNS Aliases - {e}")
        return []
    except ResourceNotFoundError as e:
        logger.warning(f"DNS Alias resource not found error - {e}")
        return []
    except HttpResponseError as e:
        logger.warning(f"Error while retrieving Azure Server DNS Aliases - {e}")
        return []

    return dns_aliases


@timeit
def get_ad_admins(credentials: Credentials, subscription_id: str, server: Dict) -> List[Dict]:
    """
    Returns details of the Server AD Administrators in a server.
    """
    try:
        client = get_client(credentials, subscription_id)
        ad_admins = list(
            map(
                lambda x: x.as_dict(),
                client.server_azure_ad_administrators.list_by_server(
                    server['resourceGroup'],
                    server['name'],
                ),
            ),
        )

    except ClientAuthenticationError as e:
        logger.warning(f"Client Authentication Error while retrieving Azure AD Administrators - {e}")
        return []
    except ResourceNotFoundError as e:
        logger.warning(f"Azure AD Administrators resource not found error - {e}")
        return []
    except HttpResponseError as e:
        logger.warning(f"Error while retrieving server azure AD Administrators - {e}")
        return []

    return ad_admins


@timeit
def get_recoverable_databases(credentials: Credentials, subscription_id: str, server: Dict) -> List[Dict]:
    """
    Returns details of the Recoverable databases in a server.
    """
    try:
        client = get_client(credentials, subscription_id)
        recoverable_databases = list(
            map(
                lambda x: x.as_dict(),
                client.recoverable_databases.list_by_server(
                    server['resourceGroup'],
                    server['name'],
                ),
            ),
        )

    except CloudError:
        # The API returns a '404 CloudError: Not Found for url: <url>' if no recoverable databases are present.
        return []
    except ClientAuthenticationError as e:
        logger.warning(f"Client Authentication Error while retrieving recoverable databases - {e}")
        return []
    except ResourceNotFoundError as e:
        logger.warning(f"Recoverable databases resource not found error - {e}")
        return []
    except HttpResponseError as e:
        logger.warning(f"Error while retrieving recoverable databases - {e}")
        return []

    return recoverable_databases


@timeit
def get_restorable_dropped_databases(credentials: Credentials, subscription_id: str, server: Dict) -> List[Dict]:
    """
    Returns details of the Restorable Dropped Databases in a server.
    """
    try:
        client = get_client(credentials, subscription_id)
        restorable_dropped_databases = list(
            map(
                lambda x: x.as_dict(),
                client.restorable_dropped_databases.list_by_server(
                    server['resourceGroup'], server['name'],
                ),
            ),
        )

    except ClientAuthenticationError as e:
        logger.warning(f"Client Authentication Error while retrieving Restorable Dropped Databases - {e}")
        return []
    except ResourceNotFoundError as e:
        logger.warning(f"Restorable Dropped Databases resource not found error - {e}")
        return []
    except HttpResponseError as e:
        logger.warning(f"Error while retrieving restorable dropped databases - {e}")
        return []

    return restorable_dropped_databases


@timeit
def get_failover_groups(credentials: Credentials, subscription_id: str, server: Dict) -> List[Dict]:
    """
    Returns details of Failover groups in a server.
    """
    try:
        client = get_client(credentials, subscription_id)
        failover_groups = list(
            map(lambda x: x.as_dict(), client.failover_groups.list_by_server(server['resourceGroup'], server['name'])),
        )

    except ClientAuthenticationError as e:
        logger.warning(f"Client Authentication Error while retrieving Failover groups - {e}")
        return []
    except ResourceNotFoundError as e:
        logger.warning(f"Failover groups resource not found error - {e}")
        return []
    except HttpResponseError as e:
        logger.warning(f"Error while retrieving failover groups - {e}")
        return []

    return failover_groups


@timeit
def get_elastic_pools(credentials: Credentials, subscription_id: str, server: Dict) -> List[Dict]:
    """
    Returns details of Elastic Pools in a server.
    """
    try:
        client = get_client(credentials, subscription_id)
        elastic_pools = list(
            map(lambda x: x.as_dict(), client.elastic_pools.list_by_server(server['resourceGroup'], server['name'])),
        )

    except ClientAuthenticationError as e:
        logger.warning(f"Client Authentication Error while retrieving Elastic Pools - {e}")
        return []
    except ResourceNotFoundError as e:
        logger.warning(f"Elastic Pools resource not found error - {e}")
        return []
    except HttpResponseError as e:
        logger.warning(f"Error while retrieving elastic pools - {e}")
        return []

    return elastic_pools


@timeit
def get_databases(credentials: Credentials, subscription_id: str, server: Dict) -> List[Dict]:
    """
    Returns details of Databases in a SQL server.
    """
    try:
        client = get_client(credentials, subscription_id)
        databases = list(
            map(lambda x: x.as_dict(), client.databases.list_by_server(server['resourceGroup'], server['name'])),
        )

    except ClientAuthenticationError as e:
        logger.warning(f"Client Authentication Error while retrieving SQL databases - {e}")
        return []
    except ResourceNotFoundError as e:
        logger.warning(f"SQL databases resource not found error - {e}")
        return []
    except HttpResponseError as e:
        logger.warning(f"Error while retrieving databases - {e}")
        return []

    return databases


@timeit
def load_server_details(
        neo4j_session: neo4j.Session, credentials: Credentials, subscription_id: str,
        details: List[Tuple[Any, Any, Any, Any, Any, Any, Any, Any, Any, Any]], update_tag: int,
) -> None:
    """
    Create dictionaries for every resource in the server so we can import them in a single query
    """
    dns_aliases = []
    ad_admins = []
    recoverable_databases = []
    restorable_dropped_databases = []
    failover_groups = []
    elastic_pools = []
    databases = []

    for server_id, name, rg, dns_alias, ad_admin, r_database, rd_database, fg, elastic_pool, database in details:
        if len(dns_alias) > 0:
            for alias in dns_alias:
                alias['server_name'] = name
                alias['server_id'] = server_id
                dns_aliases.append(alias)

        if len(ad_admin) > 0:
            for admin in ad_admin:
                admin['server_name'] = name
                admin['server_id'] = server_id
                ad_admins.append(admin)

        if len(r_database) > 0:
            for rdb in r_database:
                rdb['server_name'] = name
                rdb['server_id'] = server_id
                recoverable_databases.append(rdb)

        if len(rd_database) > 0:
            for rddb in rd_database:
                rddb['server_name'] = name
                rddb['server_id'] = server_id
                restorable_dropped_databases.append(rddb)

        if len(fg) > 0:
            for group in fg:
                group['server_name'] = name
                group['server_id'] = server_id
                failover_groups.append(group)

        if len(elastic_pool) > 0:
            for pool in elastic_pool:
                pool['server_name'] = name
                pool['server_id'] = server_id
                elastic_pools.append(pool)

        if len(database) > 0:
            for db in database:
                db['server_name'] = name
                db['server_id'] = server_id
                db['resource_group_name'] = rg
                databases.append(db)

    _load_server_dns_aliases(neo4j_session, dns_aliases, update_tag)
    _load_server_ad_admins(neo4j_session, ad_admins, update_tag)
    _load_recoverable_databases(neo4j_session, recoverable_databases, update_tag)
    _load_restorable_dropped_databases(neo4j_session, restorable_dropped_databases, update_tag)
    _load_failover_groups(neo4j_session, failover_groups, update_tag)
    _load_elastic_pools(neo4j_session, elastic_pools, update_tag)
    _load_databases(neo4j_session, databases, update_tag)

    sync_database_details(neo4j_session, credentials, subscription_id, databases, update_tag)


@timeit
def _load_server_dns_aliases(
        neo4j_session: neo4j.Session, dns_aliases: List[Dict], update_tag: int,
) -> None:
    """
    Ingest the DNS Alias details into neo4j.
    """
    ingest_dns_aliases = """
    UNWIND {dns_aliases_list} as dns_alias
    MERGE (alias:AzureServerDNSAlias{id: dns_alias.id})
    ON CREATE SET alias.firstseen = timestamp()
    SET alias.name = dns_alias.name,
    alias.dnsrecord = dns_alias.azure_dns_record,
    alias.lastupdated = {azure_update_tag}
    WITH alias, dns_alias
    MATCH (s:AzureSQLServer{id: dns_alias.server_id})
    MERGE (s)-[r:USED_BY]->(alias)
    ON CREATE SET r.firstseen = timestamp()
    SET r.lastupdated = {azure_update_tag}
    """

    neo4j_session.run(
        ingest_dns_aliases,
        dns_aliases_list=dns_aliases,
        azure_update_tag=update_tag,
    )


@timeit
def _load_server_ad_admins(
        neo4j_session: neo4j.Session, ad_admins: List[Dict], update_tag: int,
) -> None:
    """
    Ingest the Server AD Administrators details into neo4j.
    """
    ingest_ad_admins = """
    UNWIND {ad_admins_list} as ad_admin
    MERGE (a:AzureServerADAdministrator{id: ad_admin.id})
    ON CREATE SET a.firstseen = timestamp()
    SET a.name = ad_admin.name,
    a.administratortype = ad_admin.administrator_type,
    a.login = ad_admin.login,
    a.lastupdated = {azure_update_tag}
    WITH a, ad_admin
    MATCH (s:AzureSQLServer{id: ad_admin.server_id})
    MERGE (s)-[r:ADMINISTERED_BY]->(a)
    ON CREATE SET r.firstseen = timestamp()
    SET r.lastupdated = {azure_update_tag}
    """

    neo4j_session.run(
        ingest_ad_admins,
        ad_admins_list=ad_admins,
        azure_update_tag=update_tag,
    )


@timeit
def _load_recoverable_databases(
        neo4j_session: neo4j.Session, recoverable_databases: List[Dict], update_tag: int,
) -> None:
    """
    Ingest the recoverable database details into neo4j.
    """
    ingest_recoverable_databases = """
    UNWIND {recoverable_databases_list} as rec_db
    MERGE (rd:AzureRecoverableDatabase{id: rec_db.id})
    ON CREATE SET rd.firstseen = timestamp()
    SET rd.name = rec_db.name,
    rd.edition = rec_db.edition,
    rd.servicelevelobjective = rec_db.service_level_objective,
    rd.lastbackupdate = rec_db.last_available_backup_date,
    rd.lastupdated = {azure_update_tag}
    WITH rd, rec_db
    MATCH (s:AzureSQLServer{id: rec_db.server_id})
    MERGE (s)-[r:RESOURCE]->(rd)
    ON CREATE SET r.firstseen = timestamp()
    SET r.lastupdated = {azure_update_tag}
    """

    neo4j_session.run(
        ingest_recoverable_databases,
        recoverable_databases_list=recoverable_databases,
        azure_update_tag=update_tag,
    )


@timeit
def _load_restorable_dropped_databases(
        neo4j_session: neo4j.Session, restorable_dropped_databases: List[Dict], update_tag: int,
) -> None:
    """
    Ingest the restorable dropped database details into neo4j.
    """
    ingest_restorable_dropped_databases = """
    UNWIND {restorable_dropped_databases_list} as res_dropped_db
    MERGE (rdd:AzureRestorableDroppedDatabase{id: res_dropped_db.id})
    ON CREATE SET rdd.firstseen = timestamp(), rdd.location = res_dropped_db.location
    SET rdd.name = res_dropped_db.name,
    rdd.databasename = res_dropped_db.database_name,
    rdd.creationdate = res_dropped_db.creation_date,
    rdd.deletiondate = res_dropped_db.deletion_date,
    rdd.restoredate = res_dropped_db.earliest_restore_date,
    rdd.edition = res_dropped_db.edition,
    rdd.servicelevelobjective = res_dropped_db.service_level_objective,
    rdd.maxsizebytes = res_dropped_db.max_size_bytes,
    rdd.lastupdated = {azure_update_tag}
    WITH rdd, res_dropped_db
    MATCH (s:AzureSQLServer{id: res_dropped_db.server_id})
    MERGE (s)-[r:RESOURCE]->(rdd)
    ON CREATE SET r.firstseen = timestamp()
    SET r.lastupdated = {azure_update_tag}
    """

    neo4j_session.run(
        ingest_restorable_dropped_databases,
        restorable_dropped_databases_list=restorable_dropped_databases,
        azure_update_tag=update_tag,
    )


@timeit
def _load_failover_groups(
        neo4j_session: neo4j.Session, failover_groups: List[Dict], update_tag: int,
) -> None:
    """
    Ingest the failover groups details into neo4j.
    """
    ingest_failover_groups = """
    UNWIND {failover_groups_list} as fg
    MERGE (f:AzureFailoverGroup{id: fg.id})
    ON CREATE SET f.firstseen = timestamp(), f.location = fg.location
    SET f.name = fg.name,
    f.replicationrole = fg.replication_role,
    f.replicationstate = fg.replication_state,
    f.lastupdated = {azure_update_tag}
    WITH f, fg
    MATCH (s:AzureSQLServer{id: fg.server_id})
    MERGE (s)-[r:RESOURCE]->(f)
    ON CREATE SET r.firstseen = timestamp()
    SET r.lastupdated = {azure_update_tag}
    """

    neo4j_session.run(
        ingest_failover_groups,
        failover_groups_list=failover_groups,
        azure_update_tag=update_tag,
    )


@timeit
def _load_elastic_pools(
        neo4j_session: neo4j.Session, elastic_pools: List[Dict], update_tag: int,
) -> None:
    """
    Ingest the elastic pool details into neo4j.
    """
    ingest_elastic_pools = """
    UNWIND {elastic_pools_list} as ep
    MERGE (e:AzureElasticPool{id: ep.id})
    ON CREATE SET e.firstseen = timestamp(), e.location = ep.location
    SET e.name = ep.name,
    e.kind = ep.kind,
    e.creationdate = ep.creation_date,
    e.state = ep.state,
    e.maxsizebytes = ep.max_size_bytes,
    e.licensetype = ep.license_type,
    e.zoneredundant = ep.zone_redundant,
    e.lastupdated = {azure_update_tag}
    WITH e, ep
    MATCH (s:AzureSQLServer{id: ep.server_id})
    MERGE (s)-[r:RESOURCE]->(e)
    ON CREATE SET r.firstseen = timestamp()
    SET r.lastupdated = {azure_update_tag}
    """

    neo4j_session.run(
        ingest_elastic_pools,
        elastic_pools_list=elastic_pools,
        azure_update_tag=update_tag,
    )


@timeit
def _load_databases(
        neo4j_session: neo4j.Session, databases: List[Dict], update_tag: int,
) -> None:
    """
    Ingest the database details into neo4j.
    """
    ingest_databases = """
    UNWIND {databases_list} as az_database
    MERGE (d:AzureSQLDatabase{id: az_database.id})
    ON CREATE SET d.firstseen = timestamp(), d.location = az_database.location
    SET d.name = az_database.name,
    d.kind = az_database.kind,
    d.creationdate = az_database.creation_date,
    d.databaseid = az_database.database_id,
    d.maxsizebytes = az_database.max_size_bytes,
    d.licensetype = az_database.license_type,
    d.secondarylocation = az_database.default_secondary_location,
    d.elasticpoolid = az_database.elastic_pool_id,
    d.collation = az_database.collation,
    d.failovergroupid = az_database.failover_group_id,
    d.zoneredundant = az_database.zone_redundant,
    d.restorabledroppeddbid = az_database.restorable_dropped_database_id,
    d.recoverabledbid = az_database.recoverable_database_id,
    d.lastupdated = {azure_update_tag}
    WITH d, az_database
    MATCH (s:AzureSQLServer{id: az_database.server_id})
    MERGE (s)-[r:RESOURCE]->(d)
    ON CREATE SET r.firstseen = timestamp()
    SET r.lastupdated = {azure_update_tag}
    """

    neo4j_session.run(
        ingest_databases,
        databases_list=databases,
        azure_update_tag=update_tag,
    )


@timeit
def sync_database_details(
        neo4j_session: neo4j.Session, credentials: Credentials,
        subscription_id: str, databases: List[Dict], update_tag: int,
) -> None:
    db_details = get_database_details(credentials, subscription_id, databases)
    load_database_details(neo4j_session, db_details, update_tag)


@timeit
def get_database_details(
        credentials: Credentials, subscription_id: str, databases: List[Dict],
) -> Generator[Any, Any, Any]:
    """
    Iterate over the databases to get the details of resources in it.
    """
    for database in databases:
        replication_links = get_replication_links(credentials, subscription_id, database)
        db_threat_detection_policies = get_db_threat_detection_policies(credentials, subscription_id, database)
        restore_points = get_restore_points(credentials, subscription_id, database)
        transparent_data_encryptions = get_transparent_data_encryptions(credentials, subscription_id, database)
        yield database[
            'id'
        ], replication_links, db_threat_detection_policies, restore_points, transparent_data_encryptions


@timeit
def get_replication_links(credentials: Credentials, subscription_id: str, database: Dict) -> List[Dict]:
    """
    Returns the details of replication links in a database.
    """
    try:
        client = get_client(credentials, subscription_id)
        replication_links = list(
            map(
                lambda x: x.as_dict(),
                client.replication_links.list_by_database(
                    database['resource_group_name'],
                    database['server_name'],
                    database['name'],
                ),
            ),
        )

    except ClientAuthenticationError as e:
        logger.warning(f"Client Authentication Error while retrieving replication links - {e}")
        return []
    except ResourceNotFoundError as e:
        logger.warning(f"Replication links resource not found error - {e}")
        return []
    except HttpResponseError as e:
        logger.warning(f"Error while retrieving replication links - {e}")
        return []

    return replication_links


@timeit
def get_db_threat_detection_policies(credentials: Credentials, subscription_id: str, database: Dict) -> List[Dict]:
    """
    Returns the threat detection policy of a database.
    """
    try:
        client = get_client(credentials, subscription_id)
        db_threat_detection_policies = client.database_threat_detection_policies.get(
            database['resource_group_name'],
            database['server_name'],
            database['name'],
            SecurityAlertPolicyName.DEFAULT,
        ).as_dict()
    except ClientAuthenticationError as e:
        logger.warning(f"Client Authentication Error while retrieving threat detection policy - {e}")
        return []
    except ResourceNotFoundError as e:
        logger.warning(f"Threat detection policy resource not found error - {e}")
        return []
    except HttpResponseError as e:
        logger.warning(f"Error while retrieving database threat detection policies - {e}")
        return []

    return db_threat_detection_policies


@timeit
def get_restore_points(credentials: Credentials, subscription_id: str, database: Dict) -> List[Dict]:
    """
    Returns the details of restore points in a database.
    """
    try:
        client = get_client(credentials, subscription_id)
        restore_points_list = list(
            map(
                lambda x: x.as_dict(),
                client.restore_points.list_by_database(
                    database['resource_group_name'],
                    database['server_name'],
                    database['name'],
                ),
            ),
        )

    except ClientAuthenticationError as e:
        logger.warning(f"Client Authentication Error while retrieving restore points - {e}")
        return []
    except ResourceNotFoundError as e:
        logger.warning(f"Restore points resource not found error - {e}")
        return []
    except HttpResponseError as e:
        logger.warning(f"Error while retrieving restore points - {e}")
        return []

    return restore_points_list


@timeit
def get_transparent_data_encryptions(credentials: Credentials, subscription_id: str, database: Dict) -> List[Dict]:
    """
    Returns the details of transparent data encryptions in a database.
    """
    try:
        client = get_client(credentials, subscription_id)
        transparent_data_encryptions_list = client.transparent_data_encryptions.get(
            database['resource_group_name'],
            database['server_name'],
            database['name'],
            TransparentDataEncryptionName.CURRENT,
        ).as_dict()
    except ClientAuthenticationError as e:
        logger.warning(f"Client Authentication Error while retrieving transparent data encryptions - {e}")
        return []
    except ResourceNotFoundError as e:
        logger.warning(f"Transparent data encryptions resource not found error - {e}")
        return []
    except HttpResponseError as e:
        logger.warning(f"Error while retrieving transparent data encryptions - {e}")
        return []

    return transparent_data_encryptions_list


@timeit
def load_database_details(
        neo4j_session: neo4j.Session, details: List[Tuple[Any, Any, Any, Any, Any]], update_tag: int,
) -> None:
    """
    Create dictionaries for every resource in a database so we can import them in a single query
    """
    replication_links = []
    threat_detection_policies = []
    restore_points = []
    encryptions_list = []

    for databaseId, replication_link, db_threat_detection_policy, restore_point, transparent_data_encryption in details:
        if len(replication_link) > 0:
            for link in replication_link:
                link['database_id'] = databaseId
                replication_links.append(link)

        if len(db_threat_detection_policy) > 0:
            db_threat_detection_policy['database_id'] = databaseId
            threat_detection_policies.append(db_threat_detection_policy)

        if len(restore_point) > 0:
            for point in restore_point:
                point['database_id'] = databaseId
                restore_points.append(point)

        if len(transparent_data_encryption) > 0:
            transparent_data_encryption['database_id'] = databaseId
            encryptions_list.append(transparent_data_encryption)

    _load_replication_links(neo4j_session, replication_links, update_tag)
    _load_db_threat_detection_policies(neo4j_session, threat_detection_policies, update_tag)
    _load_restore_points(neo4j_session, restore_points, update_tag)
    _load_transparent_data_encryptions(neo4j_session, encryptions_list, update_tag)


@timeit
def _load_replication_links(
        neo4j_session: neo4j.Session, replication_links: List[Dict], update_tag: int,
) -> None:
    """
    Ingest replication links into neo4j.
    """
    ingest_replication_links = """
    UNWIND {replication_links_list} as replication_link
    MERGE (rl:AzureReplicationLink{id: replication_link.id})
    ON CREATE SET rl.firstseen = timestamp(),
    rl.location = replication_link.location
    SET rl.name = replication_link.name,
    rl.partnerdatabase = replication_link.partner_database,
    rl.partnerlocation = replication_link.partner_location,
    rl.partnerrole = replication_link.partner_role,
    rl.partnerserver = replication_link.partner_server,
    rl.mode = replication_link.replication_mode,
    rl.state = replication_link.replication_state,
    rl.percentcomplete = replication_link.percent_complete,
    rl.role = replication_link.role,
    rl.starttime = replication_link.start_time,
    rl.terminationallowed = replication_link.is_termination_allowed,
    rl.lastupdated = {azure_update_tag}
    WITH rl, replication_link
    MATCH (d:AzureSQLDatabase{id: replication_link.database_id})
    MERGE (d)-[r:CONTAINS]->(rl)
    ON CREATE SET r.firstseen = timestamp()
    SET r.lastupdated = {azure_update_tag}
    """

    neo4j_session.run(
        ingest_replication_links,
        replication_links_list=replication_links,
        azure_update_tag=update_tag,
    )


@timeit
def _load_db_threat_detection_policies(
        neo4j_session: neo4j.Session, threat_detection_policies: List[Dict], update_tag: int,
) -> None:
    """
    Ingest threat detection policy into neo4j.
    """
    ingest_threat_detection_policies = """
    UNWIND {threat_detection_policies_list} as tdp
    MERGE (policy:AzureDatabaseThreatDetectionPolicy{id: tdp.id})
    ON CREATE SET policy.firstseen = timestamp(),
    policy.location = tdp.location
    SET policy.name = tdp.name,
    policy.location = tdp.location,
    policy.kind = tdp.kind,
    policy.emailadmins = tdp.email_account_admins,
    policy.emailaddresses = tdp.email_addresses,
    policy.retentiondays = tdp.retention_days,
    policy.state = tdp.state,
    policy.storageendpoint = tdp.storage_endpoint,
    policy.useserverdefault = tdp.use_server_default,
    policy.disabledalerts = tdp.disabled_alerts,
    policy.lastupdated = {azure_update_tag}
    WITH policy, tdp
    MATCH (d:AzureSQLDatabase{id: tdp.database_id})
    MERGE (d)-[r:CONTAINS]->(policy)
    ON CREATE SET r.firstseen = timestamp()
    SET r.lastupdated = {azure_update_tag}
    """

    neo4j_session.run(
        ingest_threat_detection_policies,
        threat_detection_policies_list=threat_detection_policies,
        azure_update_tag=update_tag,
    )


@timeit
def _load_restore_points(
        neo4j_session: neo4j.Session, restore_points: List[Dict], update_tag: int,
) -> None:
    """
    Ingest restore points into neo4j.
    """
    ingest_restore_points = """
    UNWIND {restore_points_list} as rp
    MERGE (point:AzureRestorePoint{id: rp.id})
    ON CREATE SET point.firstseen = timestamp(),
    point.location = rp.location
    SET point.name = rp.name,
    point.restoredate = rp.earliest_restore_date,
    point.restorepointtype = rp.restore_point_type,
    point.creationdate = rp.restore_point_creation_date,
    point.lastupdated = {azure_update_tag}
    WITH point, rp
    MATCH (d:AzureSQLDatabase{id: rp.database_id})
    MERGE (d)-[r:CONTAINS]->(point)
    ON CREATE SET r.firstseen = timestamp()
    SET r.lastupdated = {azure_update_tag}
    """

    neo4j_session.run(
        ingest_restore_points,
        restore_points_list=restore_points,
        azure_update_tag=update_tag,
    )


@timeit
def _load_transparent_data_encryptions(
        neo4j_session: neo4j.Session, encryptions_list: List[Dict], update_tag: int,
) -> None:
    """
    Ingest transparent data encryptions into neo4j.
    """
    ingest_data_encryptions = """
    UNWIND {transparent_data_encryptions_list} as e
    MERGE (tae:AzureTransparentDataEncryption{id: e.id})
    ON CREATE SET tae.firstseen = timestamp(),
    tae.location = e.location
    SET tae.name = e.name,
    tae.status = e.status,
    tae.lastupdated = {azure_update_tag}
    WITH tae, e
    MATCH (d:AzureSQLDatabase{id: e.database_id})
    MERGE (d)-[r:CONTAINS]->(tae)
    ON CREATE SET r.firstseen = timestamp()
    SET r.lastupdated = {azure_update_tag}
    """

    neo4j_session.run(
        ingest_data_encryptions,
        transparent_data_encryptions_list=encryptions_list,
        azure_update_tag=update_tag,
    )


@timeit
def cleanup_azure_sql_servers(
        neo4j_session: neo4j.Session, common_job_parameters: Dict,
) -> None:
    run_cleanup_job('azure_sql_server_cleanup.json', neo4j_session, common_job_parameters)


@timeit
def sync(
        neo4j_session: neo4j.Session, credentials: Credentials, subscription_id: str,
        sync_tag: int, common_job_parameters: Dict,
) -> None:
    logger.info("Syncing Azure SQL for subscription '%s'.", subscription_id)
    server_list = get_server_list(credentials, subscription_id)
    load_server_data(neo4j_session, subscription_id, server_list, sync_tag)
    sync_server_details(neo4j_session, credentials, subscription_id, server_list, sync_tag)
    cleanup_azure_sql_servers(neo4j_session, common_job_parameters)
