using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Deployment;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;

public interface IDeploymentManagerRepository
{
    // DeploymentRecord
    /// <summary>
    /// Gets a deployment record by its ID.
    /// </summary>
    /// <param name="id">The deployment record ID.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>The deployment record, or null if not found.</returns>
    Task<DeploymentRecord?> GetDeploymentRecordByIdAsync(int id, CancellationToken cancellationToken = default);
    /// <summary>
    /// Database-side paged, filtered and instance-joined deployment listing —
    /// the read path behind the Central UI deployment-status page.
    ///
    /// <para>
    /// Replaces a <c>GetAllDeploymentRecords</c> whole-table read. That method's only
    /// caller loaded EVERY deployment record (the table is insert-only, one row per
    /// deploy attempt for the retention window) plus EVERY instance, then filtered by
    /// site scope, sorted, counted the status tiles and sliced a 25-row page — all in
    /// the Blazor circuit's memory, on every render AND on every deployment-status
    /// push. Cost scaled with the age of the system rather than the size of the page.
    /// </para>
    ///
    /// <para>
    /// <b>OFFSET paging, not keyset — deliberately.</b> The sibling Audit Log and Site
    /// Calls read paths are keyset-cursored, but this one is offset-paged like the
    /// Notification Outbox, and for the same reason: its contract surfaces a PAGE
    /// NUMBER and a TOTAL COUNT (the page renders a numbered, jump-to-any-page pager),
    /// and a keyset cursor can express neither. The total is needed regardless, since
    /// the status tiles are an aggregate over the same filtered set. The deep-offset
    /// cost that pushes high-volume tables to keyset is bounded here by
    /// <see cref="PurgeTerminalDeploymentRecordsAsync"/>, which caps the table at the
    /// terminal-record retention window — unlike the 365-day central
    /// <c>AuditLog</c>.
    /// </para>
    ///
    /// <para>
    /// Ordering is <c>DeployedAt DESC, Id DESC</c>. The <c>Id</c> tie-break is not
    /// cosmetic: <c>DeployedAt</c> ties on rapid redeploys, and an unstable sort key
    /// makes offset paging non-deterministic — rows repeat across pages or vanish
    /// between them.
    /// </para>
    /// </summary>
    /// <param name="filter">Status / search / site-scope / instance constraints; all applied DB-side.</param>
    /// <param name="pageNumber">1-based page number; values below 1 are floored at 1.</param>
    /// <param name="pageSize">Rows per page; clamped to <c>[1, DeploymentRecordSummary.MaxPageSize]</c>.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>The requested page of rows plus the total count of the filtered set.</returns>
    Task<Types.Deployment.DeploymentListPage> QueryDeploymentListPageAsync(
        Types.Deployment.DeploymentListFilter filter,
        int pageNumber,
        int pageSize,
        CancellationToken cancellationToken = default);
    /// <summary>
    /// One grouped aggregation returning the per-status row counts of the filtered
    /// set — the server-side replacement for the deployment page's four
    /// <c>records.Count(r =&gt; r.Status == X)</c> tile computations over a
    /// client-materialized table.
    ///
    /// <para>
    /// <b><see cref="Types.Deployment.DeploymentListFilter.Status"/> is ignored here.</b>
    /// The tiles are the status BREAKDOWN of the otherwise-identically-filtered set,
    /// so constraining them by the active status filter would collapse three of the
    /// four tiles to zero the moment an operator clicked one. Every other dimension
    /// (search, site scope, instance) IS applied, so the tiles always describe the
    /// same population the list is drawn from.
    /// </para>
    /// </summary>
    /// <param name="filter">The active list filter; its <c>Status</c> member is deliberately not applied.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>Per-status counts, with absent statuses reported as 0.</returns>
    Task<Types.Deployment.DeploymentStatusCounts> GetDeploymentStatusCountsAsync(
        Types.Deployment.DeploymentListFilter filter,
        CancellationToken cancellationToken = default);
    /// <summary>
    /// Database-side paged and filtered deployment query returning row summaries.
    /// Backs the <c>QueryDeployments</c> management command, which previously
    /// loaded the ENTIRE deployment-record table (insert-only, one row per deploy
    /// attempt for the life of the system) and ignored the page arguments the CLI
    /// was already sending.
    /// </summary>
    /// <param name="instanceId">Restrict to one instance's history, or <see langword="null"/> for all.</param>
    /// <param name="status">Restrict to one deployment status, or <see langword="null"/> for all.</param>
    /// <param name="instanceIdScope">
    /// When non-null, restrict to these instance ids. Used to apply a site-scoped
    /// user's permitted-site filter IN THE DATABASE rather than by loading every
    /// record and every instance and filtering in memory.
    /// </param>
    /// <param name="skip">Rows to skip; negative values are floored at 0.</param>
    /// <param name="take">Page size; non-positive or null returns every remaining row.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>One page of deployment summaries, newest first.</returns>
    Task<IReadOnlyList<Types.Deployment.DeploymentRecordSummary>> QueryDeploymentSummariesAsync(
        int? instanceId,
        DeploymentStatus? status,
        IReadOnlyCollection<int>? instanceIdScope,
        int skip,
        int? take,
        CancellationToken cancellationToken = default);
    /// <summary>
    /// Deletes TERMINAL deployment records (<see cref="DeploymentStatus.Success"/> /
    /// <see cref="DeploymentStatus.Failed"/>) that completed before
    /// <paramref name="cutoffUtc"/>, in one bounded batch.
    ///
    /// <para>
    /// Non-terminal rows are never touched: an <c>InProgress</c> record is what the
    /// query-before-redeploy reconciliation path reads to decide whether a prior
    /// deploy actually landed at the site, so purging one by age would silently
    /// disable that idempotency guard.
    /// </para>
    /// </summary>
    /// <param name="cutoffUtc">Records completing before this instant are eligible.</param>
    /// <param name="batchSize">Maximum rows deleted in this call.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>The number of rows deleted; fewer than <paramref name="batchSize"/> means the sweep is complete.</returns>
    Task<int> PurgeTerminalDeploymentRecordsAsync(DateTimeOffset cutoffUtc, int batchSize, CancellationToken cancellationToken = default);
    /// <summary>
    /// Gets all deployment records for a specific instance.
    /// </summary>
    /// <param name="instanceId">The instance ID.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A read-only list of deployment records for the instance.</returns>
    Task<IReadOnlyList<DeploymentRecord>> GetDeploymentsByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default);
    /// <summary>
    /// Gets the current deployment status for an instance.
    /// </summary>
    /// <param name="instanceId">The instance ID.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>The current deployment record, or null if no deployment exists.</returns>
    Task<DeploymentRecord?> GetCurrentDeploymentStatusAsync(int instanceId, CancellationToken cancellationToken = default);
    /// <summary>
    /// Gets a deployment record by deployment ID.
    /// </summary>
    /// <param name="deploymentId">The deployment ID.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>The deployment record, or null if not found.</returns>
    Task<DeploymentRecord?> GetDeploymentByDeploymentIdAsync(string deploymentId, CancellationToken cancellationToken = default);
    /// <summary>
    /// Adds a new deployment record.
    /// </summary>
    /// <param name="record">The deployment record to add.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    Task AddDeploymentRecordAsync(DeploymentRecord record, CancellationToken cancellationToken = default);
    /// <summary>
    /// Updates an existing deployment record.
    /// </summary>
    /// <param name="record">The deployment record to update.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    Task UpdateDeploymentRecordAsync(DeploymentRecord record, CancellationToken cancellationToken = default);
    /// <summary>
    /// Deletes a deployment record by ID, enforcing optimistic concurrency against the
    /// supplied <paramref name="expectedRowVersion"/>. The caller MUST pass the
    /// <c>RowVersion</c> it last observed on the record so EF emits
    /// <c>DELETE ... WHERE Id = @id AND RowVersion = @prior</c>. A concurrent edit
    /// surfaces as <see cref="Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException"/>
    /// on <see cref="SaveChangesAsync(CancellationToken)"/>, matching the documented
    /// "Optimistic concurrency is used on deployment status records" design rule.
    /// </summary>
    /// <param name="id">The deployment record ID to delete.</param>
    /// <param name="expectedRowVersion">The RowVersion the caller observed; used as the optimistic-concurrency token.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    Task DeleteDeploymentRecordAsync(int id, byte[] expectedRowVersion, CancellationToken cancellationToken = default);

    // SystemArtifactDeploymentRecord
    /// <summary>
    /// Gets a system artifact deployment record by ID.
    /// </summary>
    /// <param name="id">The system artifact deployment record ID.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>The system artifact deployment record, or null if not found.</returns>
    Task<SystemArtifactDeploymentRecord?> GetSystemArtifactDeploymentByIdAsync(int id, CancellationToken cancellationToken = default);
    /// <summary>
    /// Gets all system artifact deployment records.
    /// </summary>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A read-only list of all system artifact deployment records.</returns>
    Task<IReadOnlyList<SystemArtifactDeploymentRecord>> GetAllSystemArtifactDeploymentsAsync(CancellationToken cancellationToken = default);
    /// <summary>
    /// Adds a new system artifact deployment record.
    /// </summary>
    /// <param name="record">The system artifact deployment record to add.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    Task AddSystemArtifactDeploymentAsync(SystemArtifactDeploymentRecord record, CancellationToken cancellationToken = default);
    /// <summary>
    /// Updates an existing system artifact deployment record.
    /// </summary>
    /// <param name="record">The system artifact deployment record to update.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    Task UpdateSystemArtifactDeploymentAsync(SystemArtifactDeploymentRecord record, CancellationToken cancellationToken = default);
    /// <summary>
    /// Deletes a system artifact deployment record by ID.
    /// </summary>
    /// <param name="id">The system artifact deployment record ID to delete.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    Task DeleteSystemArtifactDeploymentAsync(int id, CancellationToken cancellationToken = default);

    // DeployedConfigSnapshot
    /// <summary>
    /// Gets the deployed config snapshot for an instance.
    /// </summary>
    /// <param name="instanceId">The instance ID.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>The deployed config snapshot, or null if not found.</returns>
    Task<DeployedConfigSnapshot?> GetDeployedSnapshotByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default);
    /// <summary>
    /// Adds a new deployed config snapshot.
    /// </summary>
    /// <param name="snapshot">The deployed config snapshot to add.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    Task AddDeployedSnapshotAsync(DeployedConfigSnapshot snapshot, CancellationToken cancellationToken = default);
    /// <summary>
    /// Updates an existing deployed config snapshot.
    /// </summary>
    /// <param name="snapshot">The deployed config snapshot to update.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    Task UpdateDeployedSnapshotAsync(DeployedConfigSnapshot snapshot, CancellationToken cancellationToken = default);
    /// <summary>
    /// Deletes the deployed config snapshot for an instance.
    /// </summary>
    /// <param name="instanceId">The instance ID.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    Task DeleteDeployedSnapshotAsync(int instanceId, CancellationToken cancellationToken = default);

    // Notify-and-fetch: PendingDeployment staging store
    /// <summary>
    /// Stages a flattened instance config for an in-flight deployment. Supersession:
    /// any prior pending row for the same <c>InstanceId</c> is removed first, so at most
    /// one pending row exists per instance (safe because the per-instance operation lock
    /// serializes same-instance deploys). Does NOT call <see cref="SaveChangesAsync"/> —
    /// the caller commits, mirroring the other Add/Delete repository methods.
    /// </summary>
    /// <param name="pending">The pending deployment to stage.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    Task AddPendingDeploymentAsync(PendingDeployment pending, CancellationToken cancellationToken = default);
    /// <summary>
    /// Gets a pending deployment by its deployment ID (the fetch key).
    /// </summary>
    /// <param name="deploymentId">The deployment ID.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>The pending deployment, or null if not found.</returns>
    Task<PendingDeployment?> GetPendingDeploymentByIdAsync(string deploymentId, CancellationToken cancellationToken = default);
    /// <summary>
    /// Gets the pending deployment staged for a given instance (there is at most one per instance
    /// by design — <see cref="AddPendingDeploymentAsync"/> supersedes and
    /// <see cref="StagePendingIfAbsentAsync"/> inserts only if absent). Used by startup reconcile:
    /// when <see cref="StagePendingIfAbsentAsync"/> reports a row already exists (a concurrent
    /// reconcile from the other node, or an in-flight deploy), the handler reads that existing row
    /// so a second concurrently-missing node can fetch the same pending config with its (multi-use,
    /// TTL-bound) token instead of being omitted from the gap. Defensive against >1 row: returns
    /// the most recently created one. When <paramref name="nowUtc"/> is supplied, EXPIRED rows
    /// (<c>ExpiresAtUtc &lt;= nowUtc</c>) are filtered out so the caller never receives a token the
    /// config-fetch endpoint would 404.
    /// </summary>
    /// <param name="instanceId">The instance whose pending deployment to read.</param>
    /// <param name="nowUtc">When supplied, only rows with <c>ExpiresAtUtc &gt; nowUtc</c> are considered (expired rows excluded).</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>The pending deployment for the instance, or null if none is staged.</returns>
    Task<PendingDeployment?> GetPendingDeploymentByInstanceIdAsync(int instanceId, DateTimeOffset? nowUtc = null, CancellationToken cancellationToken = default);
    /// <summary>
    /// Deletes a pending deployment by its deployment ID. No-op if not found. Does NOT
    /// call <see cref="SaveChangesAsync"/> — the caller commits, mirroring the other
    /// Add/Delete repository methods.
    /// </summary>
    /// <param name="deploymentId">The deployment ID to delete.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    Task DeletePendingDeploymentByIdAsync(string deploymentId, CancellationToken cancellationToken = default);
    /// <summary>
    /// Purges all pending deployments whose <c>ExpiresAtUtc</c> is at or before
    /// <paramref name="nowUtc"/> (TTL maintenance). This is a self-contained maintenance
    /// operation: it commits its own delete and returns the number of rows removed.
    /// </summary>
    /// <param name="nowUtc">The current UTC time; rows with <c>ExpiresAtUtc &lt;= nowUtc</c> are purged.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>The number of pending deployments purged.</returns>
    Task<int> PurgeExpiredPendingDeploymentsAsync(DateTimeOffset nowUtc, CancellationToken cancellationToken = default);

    // Startup reconciliation: expected-set query and insert-if-absent staging

    /// <summary>
    /// Returns the set of instances that central considers deployed for the given site — the join of
    /// <c>DeployedConfigSnapshot</c> (by InstanceId) with <c>Instance</c> (where SiteId == siteId).
    /// Instances without a snapshot are excluded. No <c>ConfigurationJson</c> is loaded; the full
    /// config is fetched on demand via the HTTP endpoint using a freshly-minted token from
    /// <see cref="StagePendingIfAbsentAsync"/>.
    /// </summary>
    /// <param name="siteId">The site primary key to filter on.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>All deployed instances for the site, as lightweight <see cref="ExpectedDeployment"/> projections.</returns>
    Task<IReadOnlyList<ExpectedDeployment>> GetExpectedDeploymentsForSiteAsync(int siteId, CancellationToken cancellationToken = default);

    /// <summary>
    /// Inserts a <see cref="PendingDeployment"/> for the instance ONLY IF no pending row already
    /// exists for that <c>InstanceId</c>. An existing pending row signals an in-flight deploy that
    /// is already delivering to the node — do NOT supersede it (contrast with
    /// <see cref="AddPendingDeploymentAsync"/>, which supersedes). Returns <c>true</c> if the row
    /// was staged, <c>false</c> if an existing row was found and left untouched.
    /// This method is self-contained: it commits its own save and returns a meaningful result,
    /// matching the convention of <see cref="PurgeExpiredPendingDeploymentsAsync"/>.
    /// </summary>
    /// <param name="instanceId">The instance to stage the pending deployment for.</param>
    /// <param name="deploymentId">The deployment ID (fetch key).</param>
    /// <param name="revisionHash">Revision hash of the flattened configuration.</param>
    /// <param name="configurationJson">JSON-serialized flattened configuration.</param>
    /// <param name="token">Short-TTL fetch token the site node will present to the HTTP endpoint.</param>
    /// <param name="createdAtUtc">UTC timestamp for this pending row.</param>
    /// <param name="expiresAtUtc">UTC expiry after which the token is no longer valid.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns><c>true</c> if staged; <c>false</c> if a pending row already existed and was left unchanged.</returns>
    Task<bool> StagePendingIfAbsentAsync(int instanceId, string deploymentId, string revisionHash,
        string configurationJson, string token, DateTimeOffset createdAtUtc, DateTimeOffset expiresAtUtc,
        CancellationToken cancellationToken = default);

    // Instance lookups for deployment pipeline
    /// <summary>
    /// Gets an instance by ID.
    /// </summary>
    /// <param name="instanceId">The instance ID.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>The instance, or null if not found.</returns>
    Task<Instance?> GetInstanceByIdAsync(int instanceId, CancellationToken cancellationToken = default);
    /// <summary>
    /// Gets an instance by unique name.
    /// </summary>
    /// <param name="uniqueName">The unique instance name.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>The instance, or null if not found.</returns>
    Task<Instance?> GetInstanceByUniqueNameAsync(string uniqueName, CancellationToken cancellationToken = default);
    /// <summary>
    /// Updates an instance.
    /// </summary>
    /// <param name="instance">The instance to update.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    Task UpdateInstanceAsync(Instance instance, CancellationToken cancellationToken = default);

    /// <summary>
    /// Removes an instance and everything that depends on it: deployment
    /// records, deployed config snapshot, attribute/alarm overrides, and
    /// connection bindings.
    /// </summary>
    /// <param name="instanceId">The instance ID to delete.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    Task DeleteInstanceAsync(int instanceId, CancellationToken cancellationToken = default);

    /// <summary>
    /// Saves all pending changes to the database.
    /// </summary>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task representing the number of entities saved.</returns>
    Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
