using Microsoft.Data.SqlClient;

namespace ZB.MOM.WW.GalaxyRepository;

/// <summary>
///     SQL access to the AVEVA System Platform Galaxy Repository database.
///     <para>
///     <see cref="HierarchySql" /> is the query originally ported from the OtOpcUa
///     project. <see cref="AttributesSql" /> has diverged: it additionally enumerates the
///     built-in attributes contributed by each object's primitives (from
///     <c>attribute_definition</c> via <c>primitive_instance</c>), so engine/platform objects
///     and extension sub-attributes (e.g. <c>TestAlarm001.Acked</c>) are surfaced. The
///     OtOpcUa query is not kept in sync.
///     </para>
/// </summary>
public sealed class GalaxyRepository(GalaxyRepositoryOptions options) : IGalaxyRepository
{
    /// <summary>Tests the connection to the Galaxy Repository database.</summary>
    /// <param name="ct">Token to cancel the asynchronous operation.</param>
    public async Task<bool> TestConnectionAsync(CancellationToken ct = default)
    {
        try
        {
            using SqlConnection conn = new(options.ConnectionString);
            await conn.OpenAsync(ct).ConfigureAwait(false);
            using SqlCommand cmd = new("SELECT 1", conn) { CommandTimeout = options.CommandTimeoutSeconds };
            object? result = await cmd.ExecuteScalarAsync(ct).ConfigureAwait(false);
            return result is int i && i == 1;
        }
        catch (SqlException) { return false; }
        catch (InvalidOperationException) { return false; }
    }

    /// <summary>Retrieves the last deployment time from the Galaxy Repository.</summary>
    /// <param name="ct">Token to cancel the asynchronous operation.</param>
    public async Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default)
    {
        using SqlConnection conn = new(options.ConnectionString);
        await conn.OpenAsync(ct).ConfigureAwait(false);
        using SqlCommand cmd = new("SELECT time_of_last_deploy FROM galaxy", conn)
        { CommandTimeout = options.CommandTimeoutSeconds };
        object? result = await cmd.ExecuteScalarAsync(ct).ConfigureAwait(false);
        return result is DateTime dt ? dt : null;
    }

    /// <summary>Retrieves the complete hierarchy of Galaxy objects from the repository.</summary>
    /// <param name="ct">Token to cancel the asynchronous operation.</param>
    public async Task<List<GalaxyHierarchyRow>> GetHierarchyAsync(CancellationToken ct = default)
    {
        List<GalaxyHierarchyRow> rows = new();

        using SqlConnection conn = new(options.ConnectionString);
        await conn.OpenAsync(ct).ConfigureAwait(false);

        using SqlCommand cmd = new(HierarchySql, conn) { CommandTimeout = options.CommandTimeoutSeconds };
        using SqlDataReader reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);

        while (await reader.ReadAsync(ct).ConfigureAwait(false))
        {
            string templateChainRaw = reader.IsDBNull(8) ? string.Empty : reader.GetString(8);
            string[] templateChain = templateChainRaw.Length == 0
                ? Array.Empty<string>()
                : templateChainRaw.Split(['|'], StringSplitOptions.RemoveEmptyEntries)
                    .Select(s => s.Trim())
                    .Where(s => s.Length > 0)
                    .ToArray();

            rows.Add(new GalaxyHierarchyRow
            {
                GobjectId = Convert.ToInt32(reader.GetValue(0)),
                TagName = reader.GetString(1),
                ContainedName = reader.IsDBNull(2) ? string.Empty : reader.GetString(2),
                BrowseName = reader.GetString(3),
                ParentGobjectId = Convert.ToInt32(reader.GetValue(4)),
                IsArea = Convert.ToInt32(reader.GetValue(5)) == 1,
                CategoryId = Convert.ToInt32(reader.GetValue(6)),
                HostedByGobjectId = Convert.ToInt32(reader.GetValue(7)),
                TemplateChain = templateChain,
            });
        }
        return rows;
    }

    /// <summary>Retrieves all attributes for Galaxy objects from the repository.</summary>
    /// <param name="ct">Token to cancel the asynchronous operation.</param>
    public async Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default)
    {
        List<GalaxyAttributeRow> rows = new();

        using SqlConnection conn = new(options.ConnectionString);
        await conn.OpenAsync(ct).ConfigureAwait(false);

        using SqlCommand cmd = new(AttributesSql, conn) { CommandTimeout = options.CommandTimeoutSeconds };
        using SqlDataReader reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);

        while (await reader.ReadAsync(ct).ConfigureAwait(false))
        {
            rows.Add(new GalaxyAttributeRow
            {
                GobjectId = Convert.ToInt32(reader.GetValue(0)),
                TagName = reader.GetString(1),
                AttributeName = reader.GetString(2),
                FullTagReference = reader.GetString(3),
                MxDataType = Convert.ToInt32(reader.GetValue(4)),
                DataTypeName = reader.IsDBNull(5) ? null : reader.GetString(5),
                IsArray = Convert.ToInt32(reader.GetValue(6)) == 1,
                ArrayDimension = reader.IsDBNull(7) ? null : Convert.ToInt32(reader.GetValue(7)),
                MxAttributeCategory = Convert.ToInt32(reader.GetValue(8)),
                SecurityClassification = Convert.ToInt32(reader.GetValue(9)),
                IsHistorized = Convert.ToInt32(reader.GetValue(10)) == 1,
                IsAlarm = Convert.ToInt32(reader.GetValue(11)) == 1,
            });
        }
        return rows;
    }

    /// <inheritdoc />
    public async Task<List<GalaxyAlarmAttributeRow>> GetAlarmAttributesAsync(CancellationToken ct = default)
    {
        List<GalaxyAlarmAttributeRow> rows = new();

        using SqlConnection conn = new(options.ConnectionString);
        await conn.OpenAsync(ct).ConfigureAwait(false);

        using SqlCommand cmd = new(AlarmAttributesSql, conn) { CommandTimeout = options.CommandTimeoutSeconds };
        using SqlDataReader reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);

        while (await reader.ReadAsync(ct).ConfigureAwait(false))
        {
            rows.Add(MapAlarmRow(reader.GetString(0), reader.GetString(1), reader.GetString(2)));
        }
        return rows;
    }

    /// <summary>
    ///     Maps the SQL columns projected by <c>AlarmAttributesSql</c> onto a
    ///     <see cref="GalaxyAlarmAttributeRow"/>.
    ///     <para>
    ///     <paramref name="fullTagReference"/> is the alarm-bearing attribute reference (e.g.
    ///     <c>Tank01.Level.HiHi</c>), matching the same <c>full_tag_reference</c> projection
    ///     of <see cref="AttributesSql"/> produces.
    ///     <see cref="GalaxyAlarmAttributeRow.AckCommentSubtag"/> is left empty here; the
    ///     schema does not expose an ack-comment address and the watch-list resolver
    ///     composes it later.
    ///     </para>
    ///     <paramref name="area"/> is the owning object's real Galaxy area (its alarm
    ///     group), resolved via <c>gobject.area_gobject_id</c>; the watch-list resolver
    ///     composes the canonical reference from it so the synthesized reference's group
    ///     matches what the native alarmmgr (wnwrap) emits.
    ///     Exposed internally so the derivation can be unit-tested without a database.
    /// </summary>
    /// <param name="fullTagReference">The alarm-bearing attribute reference.</param>
    /// <param name="sourceObjectReference">The owning object reference (tag name).</param>
    /// <param name="area">The owning object's Galaxy area (the alarm group).</param>
    internal static GalaxyAlarmAttributeRow MapAlarmRow(
        string fullTagReference,
        string sourceObjectReference,
        string area) => new()
        {
            FullTagReference = fullTagReference,
            SourceObjectReference = sourceObjectReference,
            Area = area,
            AckCommentSubtag = string.Empty,
        };

    // Area objects (category 13) are returned even when undeployed (deployed_package_id = 0):
    // they are organizational/model nodes that group deployed objects, so excluding them
    // orphans every area whose containing area is not itself deployed. All non-area objects
    // still require deployment. Orphans left by a missing/deleted parent area are re-rooted
    // by GalaxyHierarchyIndex.Build so nothing disappears from browse.
    private const string HierarchySql = @"
;WITH template_chain AS (
    SELECT g.gobject_id AS instance_gobject_id, t.gobject_id AS template_gobject_id,
           t.tag_name AS template_tag_name, t.derived_from_gobject_id, 0 AS depth
    FROM gobject g
    INNER JOIN gobject t ON t.gobject_id = g.derived_from_gobject_id
    WHERE g.is_template = 0 AND g.deployed_package_id <> 0 AND g.derived_from_gobject_id <> 0
    UNION ALL
    SELECT tc.instance_gobject_id, t.gobject_id, t.tag_name, t.derived_from_gobject_id, tc.depth + 1
    FROM template_chain tc
    INNER JOIN gobject t ON t.gobject_id = tc.derived_from_gobject_id
    WHERE tc.derived_from_gobject_id <> 0 AND tc.depth < 10
)
SELECT DISTINCT
    g.gobject_id,
    g.tag_name,
    g.contained_name,
    CASE WHEN g.contained_name IS NULL OR g.contained_name = ''
         THEN g.tag_name
         ELSE g.contained_name
    END AS browse_name,
    CASE WHEN g.contained_by_gobject_id = 0
         THEN g.area_gobject_id
         ELSE g.contained_by_gobject_id
    END AS parent_gobject_id,
    CASE WHEN td.category_id = 13
         THEN 1
         ELSE 0
    END AS is_area,
    td.category_id AS category_id,
    g.hosted_by_gobject_id AS hosted_by_gobject_id,
    ISNULL(
        STUFF((
            SELECT '|' + tc.template_tag_name
            FROM template_chain tc
            WHERE tc.instance_gobject_id = g.gobject_id
            ORDER BY tc.depth
            FOR XML PATH('')
        ), 1, 1, ''),
        ''
    ) AS template_chain
FROM gobject g
INNER JOIN template_definition td
    ON g.template_definition_id = td.template_definition_id
WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26)
    AND g.is_template = 0
    AND (g.deployed_package_id <> 0 OR td.category_id = 13)
ORDER BY parent_gobject_id, g.tag_name";

    // Unlike HierarchySql, this query has diverged from the OtOpcUa original. It returns two
    // kinds of attribute: user-configured dynamic attributes (the original `dynamic_attribute`
    // body, src_pri 0) and the built-in attributes every object inherits from its primitives
    // (`attribute_definition` joined through `primitive_instance`, src_pri 1). Built-in
    // attributes are why engine/platform objects and extension sub-attributes such as
    // `TestAlarm001.Acked` show up at all. Built-in rows carry no category filter (the
    // `attribute_definition` category numbering differs from `dynamic_attribute`'s — only the
    // `_`-prefix and `.Description` name exclusions apply) and are never flagged
    // `is_historized`/`is_alarm`: those flags describe a user attribute that anchors an
    // extension, not the extension's machinery leaves.
    private const string AttributesSql = @"
;WITH deployed_package_chain AS (
    SELECT g.gobject_id, p.package_id, p.derived_from_package_id, 0 AS depth
    FROM gobject g
    INNER JOIN package p ON p.package_id = g.deployed_package_id
    WHERE g.is_template = 0 AND g.deployed_package_id <> 0
    UNION ALL
    SELECT dpc.gobject_id, p.package_id, p.derived_from_package_id, dpc.depth + 1
    FROM deployed_package_chain dpc
    INNER JOIN package p ON p.package_id = dpc.derived_from_package_id
    WHERE dpc.derived_from_package_id <> 0 AND dpc.depth < 10
),
candidate AS (
    SELECT
        dpc.gobject_id, g.tag_name, da.attribute_name, da.mx_data_type, da.is_array,
        CASE WHEN da.is_array = 1
             THEN CONVERT(int, CONVERT(varbinary(2),
                  SUBSTRING(da.mx_value, 15, 2) + SUBSTRING(da.mx_value, 13, 2), 2))
             ELSE NULL END AS array_dimension,
        da.mx_attribute_category, da.security_classification, dpc.depth, 0 AS src_pri
    FROM deployed_package_chain dpc
    INNER JOIN dynamic_attribute da ON da.package_id = dpc.package_id
    INNER JOIN gobject g ON g.gobject_id = dpc.gobject_id
    INNER JOIN template_definition td ON td.template_definition_id = g.template_definition_id
    WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26)
        AND da.attribute_name NOT LIKE '[_]%'
        AND da.attribute_name NOT LIKE '%.Description'
        AND da.mx_attribute_category IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24)
    UNION ALL
    SELECT
        dpc.gobject_id, g.tag_name,
        CASE WHEN pi.primitive_name IS NULL OR pi.primitive_name = ''
             THEN ad.attribute_name
             ELSE pi.primitive_name + '.' + ad.attribute_name END AS attribute_name,
        ad.mx_data_type, ad.is_array,
        CASE WHEN ad.is_array = 1
             THEN CONVERT(int, CONVERT(varbinary(2),
                  SUBSTRING(ad.mx_value, 15, 2) + SUBSTRING(ad.mx_value, 13, 2), 2))
             ELSE NULL END AS array_dimension,
        ad.mx_attribute_category, ad.security_classification, dpc.depth, 1 AS src_pri
    FROM deployed_package_chain dpc
    INNER JOIN primitive_instance pi ON pi.package_id = dpc.package_id
    INNER JOIN attribute_definition ad ON ad.primitive_definition_id = pi.primitive_definition_id
    INNER JOIN gobject g ON g.gobject_id = dpc.gobject_id
    INNER JOIN template_definition td ON td.template_definition_id = g.template_definition_id
    WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26)
        AND ad.attribute_name NOT LIKE '[_]%'
        AND ad.attribute_name NOT LIKE '%.Description'
),
ranked AS (
    SELECT c.*, ROW_NUMBER() OVER (
        PARTITION BY c.gobject_id, c.attribute_name ORDER BY c.src_pri, c.depth) AS rn
    FROM candidate c
)
SELECT
    r.gobject_id, r.tag_name, r.attribute_name,
    r.tag_name + '.' + r.attribute_name
        + CASE WHEN r.is_array = 1 THEN '[]' ELSE '' END AS full_tag_reference,
    r.mx_data_type, dt.description AS data_type_name, r.is_array, r.array_dimension,
    r.mx_attribute_category, r.security_classification,
    CASE WHEN r.src_pri = 0 AND EXISTS (
        SELECT 1 FROM deployed_package_chain dpc2
        INNER JOIN primitive_instance pi ON pi.package_id = dpc2.package_id AND pi.primitive_name = r.attribute_name
        INNER JOIN primitive_definition pd ON pd.primitive_definition_id = pi.primitive_definition_id AND pd.primitive_name = 'HistoryExtension'
        WHERE dpc2.gobject_id = r.gobject_id
    ) THEN 1 ELSE 0 END AS is_historized,
    CASE WHEN r.src_pri = 0 AND EXISTS (
        SELECT 1 FROM deployed_package_chain dpc2
        INNER JOIN primitive_instance pi ON pi.package_id = dpc2.package_id AND pi.primitive_name = r.attribute_name
        INNER JOIN primitive_definition pd ON pd.primitive_definition_id = pi.primitive_definition_id AND pd.primitive_name = 'AlarmExtension'
        WHERE dpc2.gobject_id = r.gobject_id
    ) THEN 1 ELSE 0 END AS is_alarm
FROM ranked r
LEFT JOIN data_type dt ON dt.mx_data_type = r.mx_data_type
WHERE r.rn = 1
ORDER BY r.tag_name, r.attribute_name";

    // Returns one row per alarm-bearing attribute across all deployed objects. The three
    // projected columns (full_tag_reference, source_object_reference, area_name) are mapped
    // by MapAlarmRow. Only attributes whose owning object has an AlarmExtension primitive
    // instance matching the attribute name are returned, which is why the EXISTS correlated
    // sub-query against deployed_package_chain is needed rather than relying solely on
    // dynamic_attribute.mx_attribute_category.
    private const string AlarmAttributesSql = @"
;WITH deployed_package_chain AS (
    SELECT g.gobject_id, p.package_id, p.derived_from_package_id, 0 AS depth
    FROM gobject g
    INNER JOIN package p ON p.package_id = g.deployed_package_id
    WHERE g.is_template = 0 AND g.deployed_package_id <> 0
    UNION ALL
    SELECT dpc.gobject_id, p.package_id, p.derived_from_package_id, dpc.depth + 1
    FROM deployed_package_chain dpc
    INNER JOIN package p ON p.package_id = dpc.derived_from_package_id
    WHERE dpc.derived_from_package_id <> 0 AND dpc.depth < 10
),
candidate AS (
    SELECT dpc.gobject_id, g.tag_name, da.attribute_name, dpc.depth
    FROM deployed_package_chain dpc
    INNER JOIN dynamic_attribute da ON da.package_id = dpc.package_id
    INNER JOIN gobject g ON g.gobject_id = dpc.gobject_id
    INNER JOIN template_definition td ON td.template_definition_id = g.template_definition_id
    WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26)
        AND da.attribute_name NOT LIKE '[_]%'
        AND da.attribute_name NOT LIKE '%.Description'
        AND da.mx_attribute_category IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24)
),
ranked AS (
    SELECT c.*, ROW_NUMBER() OVER (
        PARTITION BY c.gobject_id, c.attribute_name ORDER BY c.depth) AS rn
    FROM candidate c
)
SELECT
    r.tag_name + '.' + r.attribute_name AS full_tag_reference,
    r.tag_name AS source_object_reference,
    ISNULL(area.tag_name, '') AS area_name
FROM ranked r
INNER JOIN gobject g ON g.gobject_id = r.gobject_id
LEFT JOIN gobject area ON area.gobject_id = g.area_gobject_id
WHERE r.rn = 1
    AND EXISTS (
        SELECT 1 FROM deployed_package_chain dpc2
        INNER JOIN primitive_instance pi ON pi.package_id = dpc2.package_id AND pi.primitive_name = r.attribute_name
        INNER JOIN primitive_definition pd ON pd.primitive_definition_id = pi.primitive_definition_id AND pd.primitive_name = 'AlarmExtension'
        WHERE dpc2.gobject_id = r.gobject_id
    )
ORDER BY r.tag_name, r.attribute_name";
}
