using System.Data.Common;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts;

namespace ZB.MOM.WW.OtOpcUa.Driver.Sql;

/// <summary>
///     The provider seam (design §2.2). ADO.NET's <see cref="System.Data.Common"/> base types abstract
///     connections, commands, parameters and readers; they do <b>not</b> abstract the three things that differ
///     per backend — <b>identifier quoting</b>, the <b>metadata-catalog SQL</b>, and <b>row-limit syntax</b>.
///     Those live here, so no dialect assumption leaks into the reader, the poll planner, or the schema browser.
///     <para><b>This interface is the driver's SQL-injection boundary.</b> Every runtime <em>value</em> is
///     bound as a <see cref="DbParameter"/> and never reaches a command text. Identifiers cannot be
///     parameterized in SQL, so the few that must appear as text are emitted through
///     <see cref="QuoteIdentifier"/>. Any code path that builds SQL by concatenating an authored tag field
///     <em>without</em> going through <see cref="QuoteIdentifier"/> is a defect (design §8.1).</para>
///     <para><b>Quoting is the backstop, not the only defence.</b> Design §8.1's catalog gate is
///     implemented: <see cref="SqlCatalogGate"/> resolves every authored table/column against the live
///     catalog at Initialize and <b>replaces it with the catalog's own spelling</b>, so an identifier
///     reaching <see cref="QuoteIdentifier"/> on the poll path is a string this driver read back out of
///     <see cref="ListSchemasSql"/> / <see cref="ListTablesSql"/> / <see cref="ListColumnsSql"/> — not
///     operator input. An identifier that resolves to nothing rejects its tag (which then publishes
///     <c>BadNodeIdUnknown</c>) rather than being quoted into a query against a nonexistent object.</para>
///     <para>Public because <c>Driver.Sql.Browser</c> consumes it — the catalog SQL <em>is</em> the browse
///     engine, so it is shared rather than duplicated. Implementations own their provider package;
///     <b>no provider-specific type appears in this signature</b> (<see cref="Factory"/> is the abstract
///     <see cref="DbProviderFactory"/>).</para>
/// </summary>
public interface ISqlDialect
{
    /// <summary>Which backend this dialect speaks. v1 constructs <see cref="SqlProvider.SqlServer"/> only.</summary>
    SqlProvider Provider { get; }

    /// <summary>
    ///     The provider's factory singleton, used to create connections/commands/parameters. Deliberately the
    ///     abstract base type so consumers never bind to a concrete provider package.
    /// </summary>
    DbProviderFactory Factory { get; }

    /// <summary>
    ///     Quotes <b>one</b> identifier part so it can be safely embedded in a command text, escaping the
    ///     dialect's own quote character. Rejects anything that cannot be a real catalog identifier rather
    ///     than emitting it.
    ///     <para><b>One part only.</b> A multi-part name such as <c>dbo.TagValues</c> must be split by the
    ///     caller and each part quoted separately; passing the dotted form yields a single (nonexistent)
    ///     identifier — safe, but not what the caller meant.</para>
    /// </summary>
    /// <param name="ident">The bare, unquoted identifier — as returned by the catalog, not pre-quoted.</param>
    /// <returns>The quoted identifier, ready to concatenate into a command text.</returns>
    /// <exception cref="ArgumentException">The value cannot be a valid catalog identifier.</exception>
    string QuoteIdentifier(string ident);

    /// <summary>Cheapest statement that proves the connection is alive (<c>SELECT 1</c>; Oracle needs <c>FROM DUAL</c>).</summary>
    string LivenessSql { get; }

    /// <summary>
    ///     The fragment that goes <b>immediately after <c>SELECT</c></b> to limit a statement to one row —
    ///     T-SQL's <c>"TOP 1 "</c>. Empty for a dialect that spells the limit at the end of the statement (see
    ///     <see cref="SingleRowLimitSuffix"/>).
    ///     <para><b>Include the trailing space</b> when non-empty: the planner concatenates
    ///     <c>"SELECT " + SingleRowLimitPrefix + columns</c> with no separator of its own, so an empty fragment
    ///     must leave the statement byte-identical to an unlimited one.</para>
    /// </summary>
    /// <remarks>
    ///     <para><b>Why a prefix/suffix pair rather than one <c>ApplyRowLimit(sql, n)</c> method.</b> The two
    ///     spellings sit at opposite ends of the statement — SQL Server writes
    ///     <c>SELECT TOP 1 … ORDER BY … DESC</c>, while SQLite/Postgres/MySQL write
    ///     <c>SELECT … ORDER BY … DESC LIMIT 1</c>. A single method taking the SELECT list could only serve the
    ///     prefix position; a single method taking the whole statement would move statement assembly out of the
    ///     planner and into every dialect. Two fragments keep the planner the sole author of statement shape
    ///     and let each dialect fill in the end it uses.</para>
    ///     <para><b>Why "single row" rather than a row count.</b> The only limit v1 emits is the wide-row
    ///     <c>topByTimestamp</c> selector's newest-row pick. Modelling an arbitrary <c>n</c> would be untested
    ///     surface, and <c>FETCH FIRST</c>/<c>ROWNUM</c> dialects need more than a substituted number anyway.
    ///     Widen this to a method when a feature actually needs <c>n &gt; 1</c>.</para>
    /// </remarks>
    string SingleRowLimitPrefix { get; }

    /// <summary>
    ///     The fragment appended to the <b>very end</b> of a statement to limit it to one row —
    ///     <c>" LIMIT 1"</c> for SQLite/Postgres/MySQL. Empty for a dialect that limits after <c>SELECT</c>
    ///     (see <see cref="SingleRowLimitPrefix"/>).
    ///     <para><b>Include the leading space</b> when non-empty, for the same reason the prefix carries a
    ///     trailing one: the planner appends it directly to the finished statement.</para>
    ///     <para>Exactly one of the two fragments is non-empty in every dialect modelled so far, but the planner
    ///     emits <b>both</b> unconditionally — a dialect needing both ends (or neither) is expressible without
    ///     touching the call site.</para>
    /// </summary>
    string SingleRowLimitSuffix { get; }

    /// <summary>Catalog query listing schemas — the browser's <c>RootAsync</c> level. Takes no parameters.</summary>
    string ListSchemasSql { get; }

    /// <summary>
    ///     Scalar query returning the schema an <b>unqualified</b> object name resolves to for the connecting
    ///     principal — T-SQL's <c>SELECT SCHEMA_NAME()</c>. Takes no parameters.
    /// </summary>
    /// <remarks>
    ///     <para>Needed by <see cref="SqlCatalogGate"/>: a tag authored as <c>TagValues</c> rather than
    ///     <c>dbo.TagValues</c> has to be looked up in <em>some</em> schema, and guessing <c>dbo</c> would be a
    ///     silent lie on any estate that maps its service accounts to their own default schema. Asking the
    ///     server is the only answer that matches how the poll query itself will resolve the name.</para>
    ///     <para>It is a <em>query</em> rather than a name because the answer is per-connection, not per-dialect
    ///     — the same dialect resolves differently for two different logins.</para>
    /// </remarks>
    string DefaultSchemaSql { get; }

    /// <summary>Catalog query listing tables + views in one schema — the browser's schema-expand level. Bind <c>@schema</c>.</summary>
    string ListTablesSql { get; }

    /// <summary>Catalog query listing columns of one table — the browser's table-expand / attributes level. Bind <c>@schema</c> and <c>@table</c>.</summary>
    string ListColumnsSql { get; }

    /// <summary>
    ///     Folds a catalog data-type family name (as <c>INFORMATION_SCHEMA.COLUMNS.DATA_TYPE</c> reports it —
    ///     e.g. <c>nvarchar</c>, never <c>nvarchar(50)</c>) onto a <see cref="DriverDataType"/>.
    ///     <para>Must <b>never throw</b>: a browse over a table holding one exotic column must still render.
    ///     An unrecognised family falls back to <see cref="DriverDataType.String"/>.</para>
    /// </summary>
    /// <param name="sqlDataType">The bare SQL type family name; matched case-insensitively.</param>
    /// <returns>The mapped driver data type, or <see cref="DriverDataType.String"/> when unrecognised.</returns>
    DriverDataType MapColumnType(string sqlDataType);
}
