#!/usr/bin/env python3
"""Dump design tables from ScadaLinkConfig to a replayable SQL seed file.

Usage:
    python3 infra/tools/dump_seed.py --output infra/mssql/seed-config.sql

Tables covered (insert order; reverse for delete):
    TemplateFolders, Templates, TemplateAttributes, TemplateScripts,
    TemplateAlarms, TemplateCompositions, SharedScripts, DataConnections,
    ExternalSystemDefinitions, ExternalSystemMethods

Excluded by design (per-environment, not design-time): Sites (seeded via
seed-sites.sh), Instances + InstanceConnectionBindings + InstanceOverrides,
NotificationLists/Recipients, SmtpConfigurations, ApiKeys, Areas,
SiteScopeRules, LdapGroupMappings, DataProtectionKeys, audit, deployment.
"""

import argparse
import datetime
import sys

import pymssql


DEFAULT_HOST = "localhost"
DEFAULT_PORT = 1433
DEFAULT_USER = "sa"
DEFAULT_PASSWORD = "ScadaLink_Dev1#"
DEFAULT_DATABASE = "ScadaLinkConfig"

INSERT_ORDER = [
    "TemplateFolders",
    "Templates",
    "TemplateAttributes",
    "TemplateScripts",
    "TemplateAlarms",
    "TemplateCompositions",
    "SharedScripts",
    "DataConnections",
    "ExternalSystemDefinitions",
    "ExternalSystemMethods",
]

# Identity columns get IDENTITY_INSERT wrapped around inserts and are kept in
# the column list. All listed tables happen to use Id as their identity.
IDENTITY_TABLES = set(INSERT_ORDER)

# Templates has self-FK Templates.ParentTemplateId; emit a single batch that
# inserts shallow rows first then deeper ones. pymssql returns rows in Id order
# from our ORDER BY, which matches insertion order for this schema (parent Id
# is always less than child Id in the live data).


def quote(value):
    if value is None:
        return "NULL"
    if isinstance(value, bool):
        return "1" if value else "0"
    if isinstance(value, (int, float)):
        return str(value)
    if isinstance(value, (bytes, bytearray)):
        return "0x" + value.hex()
    if isinstance(value, datetime.datetime):
        return "'" + value.isoformat(sep=" ", timespec="microseconds") + "'"
    if isinstance(value, datetime.date):
        return "'" + value.isoformat() + "'"
    if isinstance(value, datetime.time):
        return "'" + value.isoformat(timespec="microseconds") + "'"
    if isinstance(value, datetime.timedelta):
        total = value.total_seconds()
        hours, rem = divmod(int(total), 3600)
        minutes, seconds = divmod(rem, 60)
        micros = value.microseconds
        return "'{:02d}:{:02d}:{:02d}.{:06d}'".format(hours, minutes, seconds, micros)
    text = str(value).replace("'", "''")
    return "N'" + text + "'"


def get_columns(cursor, table):
    cursor.execute(
        """
        SELECT COLUMN_NAME
        FROM INFORMATION_SCHEMA.COLUMNS
        WHERE TABLE_NAME = %s
        ORDER BY ORDINAL_POSITION
        """,
        (table,),
    )
    return [row[0] for row in cursor.fetchall()]


def dump(args):
    conn = pymssql.connect(
        server=args.host,
        port=args.port,
        user=args.user,
        password=args.password,
        database=args.database,
    )
    cursor = conn.cursor()

    out = []
    out.append("-- ScadaLink design-data seed.")
    out.append("-- Auto-generated by infra/tools/dump_seed.py against " + args.database + ".")
    out.append("-- Replays the design-time configuration (templates, scripts,")
    out.append("-- data connections, external systems). Idempotent: deletes")
    out.append("-- existing rows in the covered tables before inserting.")
    out.append("--")
    out.append("-- Excluded: Sites (seed via docker/seed-sites.sh), Instances,")
    out.append("-- InstanceConnectionBindings, notifications, SMTP, API keys,")
    out.append("-- areas, LDAP mappings.")
    out.append("")
    out.append("SET NOCOUNT ON;")
    out.append("SET XACT_ABORT ON;")
    # sqlcmd defaults QUOTED_IDENTIFIER OFF; EF Core's filtered indexes
    # and computed columns require ON, so force it here.
    out.append("SET QUOTED_IDENTIFIER ON;")
    out.append("BEGIN TRAN;")
    out.append("")

    # Wipe in reverse FK order. Beyond the design tables themselves, we also
    # clear instance + deployment rows because they FK to Templates and
    # DataConnections; without this, an idempotent replay against a populated
    # DB fails on the FK to DataConnections. On a fresh reseed (after
    # teardown.sh) these tables are already empty so the DELETEs are no-ops.
    out.append("-- Wipe existing design + dependent rows so the seed is idempotent.")
    out.append("-- Order matters: dependents first.")
    delete_order = [
        # Dependents on Instances / DataConnections / Sites.
        "DeployedConfigSnapshots",
        "DeploymentRecords",
        "InstanceAlarmOverrides",
        "InstanceAttributeOverrides",
        "InstanceConnectionBindings",
        "Instances",
        # Design tables themselves.
        "ExternalSystemMethods",
        "ExternalSystemDefinitions",
        "DataConnections",
        "SharedScripts",
        "TemplateCompositions",
        # Alarms reference scripts via OnTriggerScriptId; null it first so we
        # can delete scripts without FK violations.
        "UPDATE TemplateAlarms SET OnTriggerScriptId = NULL",
        "TemplateAlarms",
        "TemplateScripts",
        "TemplateAttributes",
        # Templates is self-referential and references TemplateCompositions
        # (OwnerCompositionId); null parent links first.
        "UPDATE Templates SET ParentTemplateId = NULL, OwnerCompositionId = NULL",
        "Templates",
        # Folders is self-referential too.
        "UPDATE TemplateFolders SET ParentFolderId = NULL",
        "TemplateFolders",
    ]
    for step in delete_order:
        if step.startswith("UPDATE "):
            out.append(step + ";")
        else:
            out.append("DELETE FROM " + step + ";")
    out.append("")

    for table in INSERT_ORDER:
        columns = get_columns(cursor, table)
        if not columns:
            print("Skipping {} (no columns found)".format(table), file=sys.stderr)
            continue

        # Order by Id so self-referential rows insert in dependency order
        # (in the live data, parent Id < child Id by construction).
        order_clause = "ORDER BY Id" if "Id" in columns else ""
        cursor.execute(
            "SELECT [{}] FROM [{}] {}".format("], [".join(columns), table, order_clause)
        )
        rows = cursor.fetchall()

        out.append("-- " + table + " (" + str(len(rows)) + " rows)")
        if not rows:
            continue

        col_list = ", ".join("[" + c + "]" for c in columns)
        identity = table in IDENTITY_TABLES
        if identity:
            out.append("SET IDENTITY_INSERT [{}] ON;".format(table))
        for row in rows:
            values = ", ".join(quote(v) for v in row)
            out.append(
                "INSERT INTO [{}] ({}) VALUES ({});".format(table, col_list, values)
            )
        if identity:
            out.append("SET IDENTITY_INSERT [{}] OFF;".format(table))
        out.append("")

    out.append("COMMIT;")
    out.append("")

    sql = "\n".join(out)
    with open(args.output, "w") as f:
        f.write(sql)

    print("Wrote " + args.output + " (" + str(sum(1 for line in out if line.startswith('INSERT'))) + " inserts).")

    cursor.close()
    conn.close()


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--host", default=DEFAULT_HOST)
    parser.add_argument("--port", type=int, default=DEFAULT_PORT)
    parser.add_argument("--user", default=DEFAULT_USER)
    parser.add_argument("--password", default=DEFAULT_PASSWORD)
    parser.add_argument("--database", default=DEFAULT_DATABASE)
    parser.add_argument("--output", required=True, help="Path to write seed SQL")
    args = parser.parse_args()
    dump(args)


if __name__ == "__main__":
    main()
