const crypto = require('crypto');
const { sql, getPool } = require('./db');
const { normalizeDocumentFileName } = require('./document-file-name');

const PASSWORD_HASH_PREFIX = 'pbkdf2';
const PASSWORD_HASH_ITERATIONS = 120000;
const PASSWORD_HASH_KEY_LENGTH = 32;
const PASSWORD_HASH_DIGEST = 'sha256';
const PLACEHOLDER_PASSWORD_HASH = 'ui-placeholder-password-hash';
const EMAIL_CONFIRMATION_EXPIRES_MS = Number(process.env.EMAIL_CONFIRMATION_EXPIRES_MS || 1000 * 60 * 60 * 24);

let emailConfirmationSchemaPromise;
let applicationOpenUrlSchemaPromise;
let packageTypeSchemaPromise;
let documentSchemaPromise;

function padDatePart(value) {
  return String(value).padStart(2, '0');
}

function getLocalDateInputValue(value = new Date()) {
  const date = value instanceof Date ? value : new Date(value);
  const safeDate = Number.isNaN(date.getTime()) ? new Date() : date;

  return [
    safeDate.getFullYear(),
    padDatePart(safeDate.getMonth() + 1),
    padDatePart(safeDate.getDate())
  ].join('-');
}

function getUtcDateInputValue(value) {
  return [
    value.getUTCFullYear(),
    padDatePart(value.getUTCMonth() + 1),
    padDatePart(value.getUTCDate())
  ].join('-');
}

function parseDateInputValue(value) {
  if (typeof value !== 'string') return '';

  const match = /^(\d{4})-(\d{2})-(\d{2})(?:T.*)?$/.exec(value.trim());
  if (!match) return '';

  const year = Number(match[1]);
  const month = Number(match[2]);
  const day = Number(match[3]);
  const date = new Date(Date.UTC(year, month - 1, day));

  if (
    date.getUTCFullYear() !== year
    || date.getUTCMonth() !== month - 1
    || date.getUTCDate() !== day
  ) {
    return '';
  }

  return `${match[1]}-${match[2]}-${match[3]}`;
}

function toDateInputValue(value) {
  if (!value) return '';

  const parsedDateInput = parseDateInputValue(value);
  if (parsedDateInput) return parsedDateInput;

  const date = value instanceof Date ? value : new Date(value);
  if (Number.isNaN(date.getTime())) return '';

  return getUtcDateInputValue(date);
}

function toReleaseDateValue(value) {
  return toDateInputValue(value) || getLocalDateInputValue();
}

function toSqlDateTime2Date(value) {
  const [year, month, day] = toReleaseDateValue(value).split('-').map(Number);
  return new Date(Date.UTC(year, month - 1, day));
}

function formatDate(value) {
  if (!value) return '';
  return toDateInputValue(value);
}

function formatFileSize(bytes) {
  if (!Number.isFinite(Number(bytes)) || Number(bytes) <= 0) return '';

  const numericBytes = Number(bytes);
  const units = ['B', 'KB', 'MB', 'GB'];
  const index = Math.min(Math.floor(Math.log(numericBytes) / Math.log(1024)), units.length - 1);
  const value = numericBytes / Math.pow(1024, index);

  return `${value.toFixed(value >= 10 || index === 0 ? 0 : 1)} ${units[index]}`;
}

function normalizeRole(role) {
  return role === 'Admin' ? 'Admin' : 'User';
}

function normalizeEmail(email) {
  return String(email || '').trim().toLowerCase();
}

function isPasswordHashUsable(passwordHash) {
  return String(passwordHash || '').startsWith(`${PASSWORD_HASH_PREFIX}$`);
}

function hashPassword(password) {
  const salt = crypto.randomBytes(16).toString('base64url');
  const hash = crypto.pbkdf2Sync(
    password,
    salt,
    PASSWORD_HASH_ITERATIONS,
    PASSWORD_HASH_KEY_LENGTH,
    PASSWORD_HASH_DIGEST
  ).toString('base64url');

  return `${PASSWORD_HASH_PREFIX}$${PASSWORD_HASH_DIGEST}$${PASSWORD_HASH_ITERATIONS}$${salt}$${hash}`;
}

function verifyPassword(password, storedHash) {
  if (!isPasswordHashUsable(storedHash)) return false;

  const [prefix, digest, iterationsText, salt, expectedHash] = String(storedHash).split('$');
  const iterations = Number(iterationsText);

  if (prefix !== PASSWORD_HASH_PREFIX || !digest || !iterations || !salt || !expectedHash) {
    return false;
  }

  try {
    const actualHash = crypto.pbkdf2Sync(
      password,
      salt,
      iterations,
      Buffer.from(expectedHash, 'base64url').length,
      digest
    ).toString('base64url');
    const actualBuffer = Buffer.from(actualHash);
    const expectedBuffer = Buffer.from(expectedHash);

    return actualBuffer.length === expectedBuffer.length && crypto.timingSafeEqual(actualBuffer, expectedBuffer);
  } catch (error) {
    return false;
  }
}

function mapCurrentUserRow(row) {
  if (!row) return null;

  return {
    id: String(row.Id),
    username: row.Username,
    email: row.Email,
    name: row.FullName || row.Username,
    fullName: row.FullName || '',
    role: row.Role,
    isActive: Boolean(row.IsActive)
  };
}

function mapUserRow(row) {
  return {
    ...mapCurrentUserRow(row),
    status: row.IsActive ? 'Active' : 'Inactive',
    createdAt: formatDate(row.CreatedAt),
    updatedAt: formatDate(row.UpdatedAt),
    packageCount: Number(row.PackageCount || 0),
    applicationCount: Number(row.ApplicationCount || 0),
    documentCount: Number(row.DocumentCount || 0)
  };
}

function duplicateUserError() {
  const error = new Error('Username or email already exists.');
  error.code = 'DUPLICATE_USER';
  return error;
}

function duplicatePackageError() {
  const error = new Error('Package code or version already exists.');
  error.code = 'DUPLICATE_PACKAGE';
  return error;
}

function duplicateApplicationError() {
  const error = new Error('Application code already exists.');
  error.code = 'DUPLICATE_APPLICATION';
  return error;
}

function userHasOwnedDataError() {
  const error = new Error('User owns packages, applications, or documents.');
  error.code = 'USER_HAS_OWNED_DATA';
  return error;
}

function hashEmailToken(token) {
  return crypto.createHash('sha256').update(String(token || '')).digest('hex');
}

async function ensureEmailConfirmationSchema() {
  if (!emailConfirmationSchemaPromise) {
    emailConfirmationSchemaPromise = getPool().then((pool) => pool.request().query(`
      IF OBJECT_ID(N'dbo.EmailConfirmationTokens', N'U') IS NULL
      BEGIN
        CREATE TABLE dbo.EmailConfirmationTokens
        (
          Id UNIQUEIDENTIFIER NOT NULL
            CONSTRAINT PK_EmailConfirmationTokens PRIMARY KEY CLUSTERED
            CONSTRAINT DF_EmailConfirmationTokens_Id DEFAULT NEWSEQUENTIALID(),
          UserId UNIQUEIDENTIFIER NOT NULL,
          TokenHash CHAR(64) NOT NULL,
          ExpiresAt DATETIME2(3) NOT NULL,
          ConfirmedAt DATETIME2(3) NULL,
          CreatedAt DATETIME2(3) NOT NULL
            CONSTRAINT DF_EmailConfirmationTokens_CreatedAt DEFAULT SYSUTCDATETIME(),
          CONSTRAINT FK_EmailConfirmationTokens_User
            FOREIGN KEY (UserId) REFERENCES dbo.Users(Id) ON DELETE CASCADE
        );
      END;

      IF NOT EXISTS (
        SELECT 1
        FROM sys.indexes
        WHERE name = N'UX_EmailConfirmationTokens_TokenHash'
          AND object_id = OBJECT_ID(N'dbo.EmailConfirmationTokens')
      )
      BEGIN
        CREATE UNIQUE INDEX UX_EmailConfirmationTokens_TokenHash
        ON dbo.EmailConfirmationTokens(TokenHash);
      END;

      IF NOT EXISTS (
        SELECT 1
        FROM sys.indexes
        WHERE name = N'IX_EmailConfirmationTokens_UserId'
          AND object_id = OBJECT_ID(N'dbo.EmailConfirmationTokens')
      )
      BEGIN
        CREATE INDEX IX_EmailConfirmationTokens_UserId
        ON dbo.EmailConfirmationTokens(UserId);
      END;
    `));
  }

  return emailConfirmationSchemaPromise;
}

async function ensureApplicationOpenUrlSchema() {
  if (!applicationOpenUrlSchemaPromise) {
    applicationOpenUrlSchemaPromise = getPool().then((pool) => pool.request().query(`
      IF COL_LENGTH(N'dbo.Applications', N'OpenUrl') IS NULL
      BEGIN
        ALTER TABLE dbo.Applications
        ADD OpenUrl NVARCHAR(500) NULL;
      END;

      EXEC(N'
CREATE OR ALTER VIEW dbo.vw_ApplicationList
AS
SELECT
    a.Id,
    a.AppCode,
    a.AppName,
    a.AppVersion,
    a.Description,
    a.Status,
    a.Notes,
    a.OpenUrl,
    a.CreatedAt,
    a.UpdatedAt,
    a.CreatedByUserId,
    u.Username AS CreatedByUsername,
    COUNT_BIG(ap.Id) AS PackageCount
FROM dbo.Applications AS a
INNER JOIN dbo.Users AS u
    ON u.Id = a.CreatedByUserId
LEFT JOIN dbo.ApplicationPackages AS ap
    ON ap.ApplicationId = a.Id
GROUP BY
    a.Id,
    a.AppCode,
    a.AppName,
    a.AppVersion,
    a.Description,
    a.Status,
    a.Notes,
    a.OpenUrl,
    a.CreatedAt,
    a.UpdatedAt,
    a.CreatedByUserId,
    u.Username;
');

      EXEC(N'
CREATE OR ALTER VIEW dbo.vw_ApplicationPackageDetails
AS
SELECT
    ap.Id,
    ap.ApplicationId,
    a.AppCode,
    a.AppName,
    a.AppVersion,
    a.OpenUrl AS AppOpenUrl,
    ap.PackageId,
    p.PackageCode,
    p.PackageName,
    p.PackageType,
    ap.SelectedVersionId,
    pv.Version AS SelectedVersion,
    pv.FilePath,
    pv.DockerImage,
    ap.AddedAt,
    ap.Notes
FROM dbo.ApplicationPackages AS ap
INNER JOIN dbo.Applications AS a
    ON a.Id = ap.ApplicationId
INNER JOIN dbo.Packages AS p
    ON p.Id = ap.PackageId
LEFT JOIN dbo.PackageVersions AS pv
    ON pv.Id = ap.SelectedVersionId;
');
    `));
  }

  return applicationOpenUrlSchemaPromise;
}

async function ensurePackageTypeSchema() {
  if (!packageTypeSchemaPromise) {
    packageTypeSchemaPromise = getPool().then((pool) => pool.request().query(`
      IF EXISTS (
        SELECT 1
        FROM sys.check_constraints
        WHERE name = N'CK_Packages_PackageType'
          AND parent_object_id = OBJECT_ID(N'dbo.Packages')
          AND definition NOT LIKE N'%apt%'
      )
      BEGIN
        ALTER TABLE dbo.Packages DROP CONSTRAINT CK_Packages_PackageType;
      END;

      IF NOT EXISTS (
        SELECT 1
        FROM sys.check_constraints
        WHERE name = N'CK_Packages_PackageType'
          AND parent_object_id = OBJECT_ID(N'dbo.Packages')
      )
      BEGIN
        ALTER TABLE dbo.Packages WITH CHECK
        ADD CONSTRAINT CK_Packages_PackageType
        CHECK (PackageType IN (N'deb', N'apt', N'docker'));
      END;
    `));
  }

  return packageTypeSchemaPromise;
}

async function ensureDocumentSchema() {
  if (!documentSchemaPromise) {
    documentSchemaPromise = getPool().then((pool) => pool.request().query(`
      IF OBJECT_ID(N'dbo.Documents', N'U') IS NULL
      BEGIN
        CREATE TABLE dbo.Documents
        (
          Id UNIQUEIDENTIFIER NOT NULL
            CONSTRAINT PK_Documents PRIMARY KEY CLUSTERED
            CONSTRAINT DF_Documents_Id DEFAULT NEWSEQUENTIALID(),
          Title NVARCHAR(200) NOT NULL,
          Category NVARCHAR(50) NOT NULL
            CONSTRAINT DF_Documents_Category DEFAULT N'other',
          Summary NVARCHAR(1000) NULL,
          Content NVARCHAR(MAX) NULL,
          FilePath NVARCHAR(1000) NULL,
          OriginalFileName NVARCHAR(260) NULL,
          MimeType NVARCHAR(200) NULL,
          FileSizeBytes BIGINT NULL,
          CreatedByUserId UNIQUEIDENTIFIER NOT NULL,
          CreatedAt DATETIME2(3) NOT NULL
            CONSTRAINT DF_Documents_CreatedAt DEFAULT SYSUTCDATETIME(),
          UpdatedAt DATETIME2(3) NULL,
          CONSTRAINT FK_Documents_CreatedByUser
            FOREIGN KEY (CreatedByUserId) REFERENCES dbo.Users(Id),
          CONSTRAINT CK_Documents_Title_NotBlank CHECK (LEN(LTRIM(RTRIM(Title))) > 0),
          CONSTRAINT CK_Documents_Category CHECK (
            Category IN (N'introduction', N'guide', N'user-guide', N'technical', N'policy', N'other')
          ),
          CONSTRAINT CK_Documents_FileSizeBytes CHECK (FileSizeBytes IS NULL OR FileSizeBytes >= 0),
          CONSTRAINT CK_Documents_HasReadableContent CHECK (
            NULLIF(LTRIM(RTRIM(Content)), N'') IS NOT NULL OR FilePath IS NOT NULL
          )
        );
      END;

      IF NOT EXISTS (
        SELECT 1
        FROM sys.indexes
        WHERE name = N'IX_Documents_Category_UpdatedAt'
          AND object_id = OBJECT_ID(N'dbo.Documents')
      )
      BEGIN
        CREATE INDEX IX_Documents_Category_UpdatedAt
        ON dbo.Documents(Category, UpdatedAt DESC, CreatedAt DESC);
      END;

      IF NOT EXISTS (
        SELECT 1
        FROM sys.indexes
        WHERE name = N'IX_Documents_CreatedByUserId'
          AND object_id = OBJECT_ID(N'dbo.Documents')
      )
      BEGIN
        CREATE INDEX IX_Documents_CreatedByUserId
        ON dbo.Documents(CreatedByUserId);
      END;
    `));
  }

  return documentSchemaPromise;
}

function normalizePackageStatus(isActive) {
  return isActive ? 'Active' : 'Archived';
}

function normalizeApplicationStatus(status) {
  return ['Draft', 'Released', 'Archived'].includes(status) ? status : 'Draft';
}

function normalizeVersionStatus(row) {
  if (row.IsLatest) return 'Latest';
  if (row.IsDeprecated) return 'Deprecated';
  return 'Stable';
}

function mapPackageRow(row) {
  return {
    id: String(row.Id),
    code: row.PackageCode,
    name: row.PackageName,
    type: row.PackageType,
    latestVersionId: row.LatestVersionId ? String(row.LatestVersionId) : '',
    latestVersion: row.LatestVersion || '',
    latestReleaseDate: formatDate(row.LatestReleaseDate),
    status: normalizePackageStatus(row.IsActive),
    owner: row.CreatedByUsername || '',
    description: row.Description || '',
    artifact: row.LatestFilePath || row.LatestDockerImage || '',
    versionCount: Number(row.VersionCount || 0),
    versions: []
  };
}

function mapVersionRow(row) {
  return {
    id: String(row.Id),
    packageId: String(row.PackageId),
    version: row.Version,
    releaseDate: formatDate(row.ReleaseDate),
    uploadedBy: row.UploadedBy || '',
    status: normalizeVersionStatus(row),
    size: formatFileSize(row.FileSizeBytes),
    changeLog: row.ChangeLog || '',
    filePath: row.FilePath || '',
    dockerImage: row.DockerImage || '',
    checksum: row.FileChecksumSha256 || ''
  };
}

function mapApplicationRow(row) {
  return {
    id: String(row.Id),
    code: row.AppCode,
    name: row.AppName,
    version: row.AppVersion,
    status: row.Status,
    createdAt: formatDate(row.CreatedAt),
    createdBy: row.CreatedByUsername || '',
    notes: row.Notes || row.Description || '',
    openUrl: row.OpenUrl || '',
    packageCount: Number(row.PackageCount || 0),
    packages: []
  };
}

function mapApplicationPackageRow(row) {
  return {
    id: String(row.Id),
    applicationId: String(row.ApplicationId),
    appOpenUrl: row.AppOpenUrl || '',
    packageId: String(row.PackageId),
    code: row.PackageCode,
    name: row.PackageName,
    type: row.PackageType,
    selectedVersionId: row.SelectedVersionId ? String(row.SelectedVersionId) : '',
    selectedVersion: row.SelectedVersion || '',
    filePath: row.FilePath || '',
    dockerImage: row.DockerImage || ''
  };
}

function mapDocumentRow(row) {
  if (!row) return null;

  return {
    id: String(row.Id),
    title: row.Title,
    category: row.Category,
    summary: row.Summary || '',
    content: row.Content || '',
    hasContent: row.HasContent === undefined ? Boolean(row.Content) : Boolean(row.HasContent),
    filePath: row.FilePath || '',
    originalFileName: normalizeDocumentFileName(row.OriginalFileName || ''),
    mimeType: row.MimeType || '',
    fileSizeBytes: Number(row.FileSizeBytes || 0),
    fileSize: formatFileSize(row.FileSizeBytes),
    createdByUserId: row.CreatedByUserId ? String(row.CreatedByUserId) : '',
    createdBy: row.CreatedByUsername || '',
    createdAt: formatDate(row.CreatedAt),
    updatedAt: formatDate(row.UpdatedAt || row.CreatedAt)
  };
}

function isLoopbackHost(hostname) {
  const host = String(hostname || '').toLowerCase();
  return host === 'localhost' || host === '127.0.0.1' || host === '::1';
}

function toAbsoluteUrl(baseUrl, filePath) {
  if (!filePath) return '';
  const normalizedBaseUrl = String(baseUrl || '').replace(/\/+$/, '');

  if (/^https?:\/\//i.test(filePath)) {
    try {
      const parsed = new URL(filePath);
      if (isLoopbackHost(parsed.hostname) && parsed.pathname.startsWith('/uploads/')) {
        return `${normalizedBaseUrl}${parsed.pathname}${parsed.search}`;
      }
    } catch {
      return filePath;
    }

    return filePath;
  }

  const normalizedPath = String(filePath).startsWith('/') ? filePath : `/${filePath}`;

  return `${normalizedBaseUrl}${normalizedPath}`;
}

function inferDockerPortsFromOpenUrl(openUrl) {
  if (!openUrl) return [];

  try {
    const parsed = new URL(openUrl);
    if (!isLoopbackHost(parsed.hostname) || !parsed.port) {
      return [];
    }

    return [`${parsed.port}:${parsed.port}`];
  } catch {
    return [];
  }
}

async function getUserById(id) {
  const pool = await getPool();
  const result = await pool.request()
    .input('Id', sql.NVarChar(100), id)
    .query(`
      SELECT TOP (1) Id, Username, Email, FullName, Role, IsActive
      FROM dbo.Users
      WHERE CONVERT(NVARCHAR(36), Id) = @Id;
    `);

  return mapCurrentUserRow(result.recordset[0]);
}

async function getUserOwnershipCounts(userId) {
  await ensureDocumentSchema();
  const pool = await getPool();
  const result = await pool.request()
    .input('UserId', sql.UniqueIdentifier, userId)
    .query(`
      SELECT
        (SELECT COUNT_BIG(*) FROM dbo.Packages WHERE CreatedByUserId = @UserId) AS PackageCount,
        (SELECT COUNT_BIG(*) FROM dbo.Applications WHERE CreatedByUserId = @UserId) AS ApplicationCount,
        (SELECT COUNT_BIG(*) FROM dbo.Documents WHERE CreatedByUserId = @UserId) AS DocumentCount;
    `);
  const row = result.recordset[0];

  return {
    packageCount: Number(row.PackageCount || 0),
    applicationCount: Number(row.ApplicationCount || 0),
    documentCount: Number(row.DocumentCount || 0)
  };
}

async function listUsers() {
  await ensureDocumentSchema();
  const pool = await getPool();
  const result = await pool.request().query(`
    SELECT
      u.Id,
      u.Username,
      u.Email,
      u.FullName,
      u.Role,
      u.IsActive,
      u.CreatedAt,
      u.UpdatedAt,
      package_count.PackageCount,
      application_count.ApplicationCount,
      document_count.DocumentCount
    FROM dbo.Users AS u
    OUTER APPLY (
      SELECT COUNT_BIG(*) AS PackageCount
      FROM dbo.Packages AS p
      WHERE p.CreatedByUserId = u.Id
    ) AS package_count
    OUTER APPLY (
      SELECT COUNT_BIG(*) AS ApplicationCount
      FROM dbo.Applications AS a
      WHERE a.CreatedByUserId = u.Id
    ) AS application_count
    OUTER APPLY (
      SELECT COUNT_BIG(*) AS DocumentCount
      FROM dbo.Documents AS d
      WHERE d.CreatedByUserId = u.Id
    ) AS document_count
    ORDER BY u.CreatedAt DESC, u.Username ASC;
  `);

  return result.recordset.map(mapUserRow);
}

async function countUsableUsers() {
  const pool = await getPool();
  const result = await pool.request()
    .input('PasswordHashPrefix', sql.NVarChar(50), `${PASSWORD_HASH_PREFIX}$%`)
    .query(`
      SELECT COUNT_BIG(*) AS Total
      FROM dbo.Users
      WHERE PasswordHash LIKE @PasswordHashPrefix;
    `);

  return Number(result.recordset[0].Total || 0);
}

async function findUserAccountByIdentifier(identifier) {
  const pool = await getPool();
  const result = await pool.request()
    .input('Identifier', sql.NVarChar(255), String(identifier || '').trim())
    .query(`
      SELECT TOP (1) Id, Username, Email, PasswordHash, FullName, Role, IsActive
      FROM dbo.Users
      WHERE Username = @Identifier OR Email = @Identifier;
    `);

  return result.recordset[0] || null;
}

async function getPendingUserByEmail(email) {
  const pool = await getPool();
  const result = await pool.request()
    .input('Email', sql.NVarChar(255), normalizeEmail(email))
    .input('PasswordHashPrefix', sql.NVarChar(50), `${PASSWORD_HASH_PREFIX}$%`)
    .query(`
      SELECT TOP (1) Id, Username, Email, FullName, Role, IsActive
      FROM dbo.Users
      WHERE Email = @Email
        AND IsActive = 0
        AND PasswordHash LIKE @PasswordHashPrefix;
    `);

  return mapCurrentUserRow(result.recordset[0]);
}

async function getRegistrationConflict({ username, email }) {
  const pool = await getPool();
  const result = await pool.request()
    .input('Username', sql.NVarChar(100), String(username || '').trim())
    .input('Email', sql.NVarChar(255), normalizeEmail(email))
    .query(`
      SELECT TOP (1) Id, Username, Email, FullName, Role, IsActive
      FROM dbo.Users
      WHERE Username = @Username OR Email = @Email
      ORDER BY
        CASE
          WHEN Email = @Email THEN 0
          WHEN Username = @Username THEN 1
          ELSE 2
        END,
        CreatedAt ASC;
    `);
  const user = mapCurrentUserRow(result.recordset[0]);

  if (!user) return null;

  return {
    user,
    field: normalizeEmail(user.email) === normalizeEmail(email) ? 'email' : 'username'
  };
}

async function authenticateUser(identifier, password) {
  const row = await findUserAccountByIdentifier(identifier);
  if (!row || !row.IsActive) return null;
  if (!verifyPassword(String(password || ''), row.PasswordHash)) return null;

  return mapCurrentUserRow(row);
}

async function createUser(input) {
  const pool = await getPool();
  const username = String(input.username || '').trim();
  const email = normalizeEmail(input.email);
  const fullName = String(input.fullName || '').trim() || null;
  const passwordHash = hashPassword(String(input.password || ''));
  const usableUserCount = await countUsableUsers();
  const role = input.role ? normalizeRole(input.role) : (usableUserCount === 0 ? 'Admin' : 'User');
  const isActive = input.isActive === undefined ? true : Boolean(input.isActive);

  const existingResult = await pool.request()
    .input('Username', sql.NVarChar(100), username)
    .input('Email', sql.NVarChar(255), email)
    .query(`
      SELECT TOP (1) Id, PasswordHash
      FROM dbo.Users
      WHERE Username = @Username OR Email = @Email;
    `);
  const existing = existingResult.recordset[0];

  if (existing) {
    if (usableUserCount === 0 && existing.PasswordHash === PLACEHOLDER_PASSWORD_HASH) {
      const updateResult = await pool.request()
        .input('Id', sql.UniqueIdentifier, existing.Id)
        .input('Username', sql.NVarChar(100), username)
        .input('Email', sql.NVarChar(255), email)
        .input('PasswordHash', sql.NVarChar(500), passwordHash)
        .input('FullName', sql.NVarChar(200), fullName)
        .input('Role', sql.NVarChar(50), role)
        .input('IsActive', sql.Bit, isActive ? 1 : 0)
        .query(`
          UPDATE dbo.Users
          SET Username = @Username,
              Email = @Email,
              PasswordHash = @PasswordHash,
              FullName = @FullName,
              Role = @Role,
              IsActive = @IsActive,
              UpdatedAt = SYSUTCDATETIME()
          OUTPUT inserted.Id, inserted.Username, inserted.Email, inserted.FullName, inserted.Role, inserted.IsActive
          WHERE Id = @Id;
        `);

      return mapCurrentUserRow(updateResult.recordset[0]);
    }

    throw duplicateUserError();
  }

  if (usableUserCount === 0) {
    const placeholderResult = await pool.request()
      .input('PasswordHash', sql.NVarChar(500), PLACEHOLDER_PASSWORD_HASH)
      .query(`
        SELECT TOP (1) Id
        FROM dbo.Users
        WHERE PasswordHash = @PasswordHash
        ORDER BY CreatedAt ASC;
      `);
    const placeholder = placeholderResult.recordset[0];

    if (placeholder) {
      const updateResult = await pool.request()
        .input('Id', sql.UniqueIdentifier, placeholder.Id)
        .input('Username', sql.NVarChar(100), username)
        .input('Email', sql.NVarChar(255), email)
        .input('PasswordHash', sql.NVarChar(500), passwordHash)
        .input('FullName', sql.NVarChar(200), fullName)
        .input('Role', sql.NVarChar(50), role)
        .input('IsActive', sql.Bit, isActive ? 1 : 0)
        .query(`
          UPDATE dbo.Users
          SET Username = @Username,
              Email = @Email,
              PasswordHash = @PasswordHash,
              FullName = @FullName,
              Role = @Role,
              IsActive = @IsActive,
              UpdatedAt = SYSUTCDATETIME()
          OUTPUT inserted.Id, inserted.Username, inserted.Email, inserted.FullName, inserted.Role, inserted.IsActive
          WHERE Id = @Id;
        `);

      return mapCurrentUserRow(updateResult.recordset[0]);
    }
  }

  try {
    const result = await pool.request()
      .input('Username', sql.NVarChar(100), username)
      .input('Email', sql.NVarChar(255), email)
      .input('PasswordHash', sql.NVarChar(500), passwordHash)
      .input('FullName', sql.NVarChar(200), fullName)
      .input('Role', sql.NVarChar(50), role)
      .input('IsActive', sql.Bit, isActive ? 1 : 0)
      .query(`
        INSERT dbo.Users (Username, Email, PasswordHash, FullName, Role, IsActive)
        OUTPUT inserted.Id, inserted.Username, inserted.Email, inserted.FullName, inserted.Role, inserted.IsActive
        VALUES (@Username, @Email, @PasswordHash, @FullName, @Role, @IsActive);
      `);

    return mapCurrentUserRow(result.recordset[0]);
  } catch (error) {
    if (error.number === 2601 || error.number === 2627) {
      throw duplicateUserError();
    }

    throw error;
  }
}

async function createEmailConfirmationToken(userId) {
  await ensureEmailConfirmationSchema();

  const pool = await getPool();
  const token = crypto.randomBytes(32).toString('base64url');
  const tokenHash = hashEmailToken(token);
  const expiresAt = new Date(Date.now() + EMAIL_CONFIRMATION_EXPIRES_MS);

  await pool.request()
    .input('UserId', sql.UniqueIdentifier, userId)
    .input('TokenHash', sql.Char(64), tokenHash)
    .input('ExpiresAt', sql.DateTime2, expiresAt)
    .query(`
      DELETE FROM dbo.EmailConfirmationTokens
      WHERE UserId = @UserId
        AND ConfirmedAt IS NULL;

      INSERT dbo.EmailConfirmationTokens (UserId, TokenHash, ExpiresAt)
      VALUES (@UserId, @TokenHash, @ExpiresAt);
    `);

  return token;
}

async function confirmEmailToken(token) {
  await ensureEmailConfirmationSchema();

  const pool = await getPool();
  const result = await pool.request()
    .input('TokenHash', sql.Char(64), hashEmailToken(token))
    .query(`
      SET NOCOUNT ON;
      SET XACT_ABORT ON;

      BEGIN TRANSACTION;

      DECLARE @TokenId UNIQUEIDENTIFIER;
      DECLARE @UserId UNIQUEIDENTIFIER;

      SELECT TOP (1)
        @TokenId = Id,
        @UserId = UserId
      FROM dbo.EmailConfirmationTokens WITH (UPDLOCK, HOLDLOCK)
      WHERE TokenHash = @TokenHash
        AND ConfirmedAt IS NULL
        AND ExpiresAt >= SYSUTCDATETIME();

      IF @TokenId IS NULL
      BEGIN
        COMMIT TRANSACTION;
        SELECT TOP (0) Id, Username, Email, FullName, Role, IsActive
        FROM dbo.Users;
        RETURN;
      END;

      UPDATE dbo.EmailConfirmationTokens
      SET ConfirmedAt = SYSUTCDATETIME()
      WHERE Id = @TokenId;

      UPDATE dbo.Users
      SET IsActive = 1,
          UpdatedAt = SYSUTCDATETIME()
      OUTPUT inserted.Id, inserted.Username, inserted.Email, inserted.FullName, inserted.Role, inserted.IsActive
      WHERE Id = @UserId;

      COMMIT TRANSACTION;
    `);

  return mapCurrentUserRow(result.recordset[0]);
}

async function updateUserAccess(input) {
  const pool = await getPool();
  await pool.request()
    .input('Id', sql.UniqueIdentifier, input.userId)
    .input('Role', sql.NVarChar(50), normalizeRole(input.role))
    .input('IsActive', sql.Bit, input.isActive ? 1 : 0)
    .query(`
      UPDATE dbo.Users
      SET Role = @Role,
          IsActive = @IsActive,
          UpdatedAt = SYSUTCDATETIME()
      WHERE Id = @Id;
    `);
}

async function updateUser(input) {
  const pool = await getPool();
  const username = String(input.username || '').trim();
  const email = normalizeEmail(input.email);
  const fullName = String(input.fullName || '').trim() || null;
  const role = normalizeRole(input.role);
  const isActive = input.isActive ? 1 : 0;
  const passwordHash = input.password ? hashPassword(String(input.password)) : null;

  const existingResult = await pool.request()
    .input('Id', sql.UniqueIdentifier, input.userId)
    .input('Username', sql.NVarChar(100), username)
    .input('Email', sql.NVarChar(255), email)
    .query(`
      SELECT TOP (1) Id
      FROM dbo.Users
      WHERE (Username = @Username OR Email = @Email)
        AND Id <> @Id;
    `);

  if (existingResult.recordset.length > 0) {
    throw duplicateUserError();
  }

  const result = await pool.request()
    .input('Id', sql.UniqueIdentifier, input.userId)
    .input('Username', sql.NVarChar(100), username)
    .input('Email', sql.NVarChar(255), email)
    .input('FullName', sql.NVarChar(200), fullName)
    .input('Role', sql.NVarChar(50), role)
    .input('IsActive', sql.Bit, isActive)
    .input('PasswordHash', sql.NVarChar(500), passwordHash)
    .query(`
      UPDATE dbo.Users
      SET Username = @Username,
          Email = @Email,
          FullName = @FullName,
          Role = @Role,
          IsActive = @IsActive,
          PasswordHash = COALESCE(@PasswordHash, PasswordHash),
          UpdatedAt = SYSUTCDATETIME()
      OUTPUT inserted.Id, inserted.Username, inserted.Email, inserted.FullName, inserted.Role, inserted.IsActive
      WHERE Id = @Id;
    `);

  return mapCurrentUserRow(result.recordset[0]);
}

async function deleteUser(userId) {
  const counts = await getUserOwnershipCounts(userId);

  if (counts.packageCount > 0 || counts.applicationCount > 0 || counts.documentCount > 0) {
    throw userHasOwnedDataError();
  }

  const pool = await getPool();
  const result = await pool.request()
    .input('Id', sql.UniqueIdentifier, userId)
    .query(`
      DELETE FROM dbo.Users
      OUTPUT deleted.Id
      WHERE Id = @Id;
    `);

  return result.recordset.length > 0;
}

async function listPackages() {
  await ensurePackageTypeSchema();
  const pool = await getPool();
  const result = await pool.request().query(`
    SELECT *
    FROM dbo.vw_PackageList
    ORDER BY CreatedAt DESC, PackageName ASC;
  `);

  return result.recordset.map(mapPackageRow);
}

async function listPackageVersions() {
  const pool = await getPool();
  const result = await pool.request().query(`
    SELECT *
    FROM dbo.vw_PackageVersionList
    ORDER BY PackageId, IsLatest DESC, ReleaseDate DESC, UploadedAt DESC;
  `);

  return result.recordset.map(mapVersionRow);
}

async function attachPackageVersions(packages) {
  if (packages.length === 0) return packages;

  const versions = await listPackageVersions();
  const versionsByPackageId = new Map();

  versions.forEach((version) => {
    if (!versionsByPackageId.has(version.packageId)) {
      versionsByPackageId.set(version.packageId, []);
    }

    versionsByPackageId.get(version.packageId).push(version);
  });

  return packages.map((packageItem) => ({
    ...packageItem,
    versions: versionsByPackageId.get(packageItem.id) || []
  }));
}

async function getPackageById(id) {
  const pool = await getPool();
  const packageResult = await pool.request()
    .input('Id', sql.NVarChar(100), id)
    .query(`
      SELECT TOP (1) *
      FROM dbo.vw_PackageList
      WHERE CONVERT(NVARCHAR(36), Id) = @Id OR PackageCode = @Id;
    `);

  if (packageResult.recordset.length === 0) return null;

  const packageItem = mapPackageRow(packageResult.recordset[0]);
  const versions = await pool.request()
    .input('PackageId', sql.UniqueIdentifier, packageItem.id)
    .query(`
      SELECT *
      FROM dbo.vw_PackageVersionList
      WHERE PackageId = @PackageId
      ORDER BY IsLatest DESC, ReleaseDate DESC, UploadedAt DESC;
    `);

  packageItem.versions = versions.recordset.map(mapVersionRow);
  return packageItem;
}

async function listApplications() {
  await ensureApplicationOpenUrlSchema();
  const pool = await getPool();
  const result = await pool.request().query(`
    SELECT *
    FROM dbo.vw_ApplicationList
    ORDER BY CreatedAt DESC, AppName ASC;
  `);

  const applications = result.recordset.map(mapApplicationRow);
  return attachApplicationPackages(applications);
}

async function listApplicationPackages(applicationId) {
  await ensureApplicationOpenUrlSchema();
  const pool = await getPool();
  const request = pool.request();
  let whereClause = '';

  if (applicationId) {
    request.input('ApplicationId', sql.UniqueIdentifier, applicationId);
    whereClause = 'WHERE ApplicationId = @ApplicationId';
  }

  const result = await request.query(`
    SELECT *
    FROM dbo.vw_ApplicationPackageDetails
    ${whereClause}
    ORDER BY AppName ASC, PackageName ASC;
  `);

  return result.recordset.map(mapApplicationPackageRow);
}

async function attachApplicationPackages(applications) {
  if (applications.length === 0) return applications;

  const applicationPackages = await listApplicationPackages();
  const packagesByApplicationId = new Map();

  applicationPackages.forEach((packageItem) => {
    if (!packagesByApplicationId.has(packageItem.applicationId)) {
      packagesByApplicationId.set(packageItem.applicationId, []);
    }

    packagesByApplicationId.get(packageItem.applicationId).push(packageItem);
  });

  return applications.map((application) => {
    const packages = packagesByApplicationId.get(application.id) || [];

    return {
      ...application,
      packageCount: packages.length,
      packages
    };
  });
}

async function getApplicationById(id) {
  await ensureApplicationOpenUrlSchema();
  const pool = await getPool();
  const appResult = await pool.request()
    .input('Id', sql.NVarChar(100), id)
    .query(`
      SELECT TOP (1) *
      FROM dbo.vw_ApplicationList
      WHERE CONVERT(NVARCHAR(36), Id) = @Id OR AppCode = @Id;
    `);

  if (appResult.recordset.length === 0) return null;

  const application = mapApplicationRow(appResult.recordset[0]);
  application.packages = await listApplicationPackages(application.id);
  application.packageCount = application.packages.length;
  return application;
}

async function getApplicationManifest(appCode, version, baseUrl) {
  await Promise.all([
    ensureApplicationOpenUrlSchema(),
    ensurePackageTypeSchema()
  ]);
  const pool = await getPool();
  const appResult = await pool.request()
    .input('AppCode', sql.NVarChar(100), String(appCode || '').trim())
    .input('AppVersion', sql.NVarChar(50), String(version || '').trim())
    .query(`
      SELECT TOP (1) Id, AppCode, AppName, AppVersion, OpenUrl
      FROM dbo.Applications
      WHERE (CONVERT(NVARCHAR(36), Id) = @AppCode OR AppCode = @AppCode)
        AND AppVersion = @AppVersion
        AND Status = N'Released';
    `);

  const appRow = appResult.recordset[0];
  if (!appRow) return null;

  const componentResult = await pool.request()
    .input('ApplicationId', sql.UniqueIdentifier, appRow.Id)
    .query(`
      SELECT
        ap.Id,
        p.PackageCode,
        p.PackageName,
        p.PackageType,
        COALESCE(selected_version.Id, latest_version.Id) AS PackageVersionId,
        COALESCE(selected_version.Version, latest_version.Version) AS Version,
        COALESCE(selected_version.FilePath, latest_version.FilePath) AS FilePath,
        COALESCE(selected_version.DockerImage, latest_version.DockerImage) AS DockerImage,
        COALESCE(selected_version.FileChecksumSha256, latest_version.FileChecksumSha256) AS FileChecksumSha256,
        ROW_NUMBER() OVER (ORDER BY ap.AddedAt ASC, p.PackageCode ASC) * 10 AS InstallOrder
      FROM dbo.ApplicationPackages AS ap
      INNER JOIN dbo.Packages AS p
        ON p.Id = ap.PackageId
      LEFT JOIN dbo.PackageVersions AS selected_version
        ON selected_version.Id = ap.SelectedVersionId
      OUTER APPLY (
        SELECT TOP (1) latest.*
        FROM dbo.PackageVersions AS latest
        WHERE latest.PackageId = p.Id
          AND latest.IsLatest = 1
        ORDER BY latest.ReleaseDate DESC, latest.UploadedAt DESC
      ) AS latest_version
      WHERE ap.ApplicationId = @ApplicationId
      ORDER BY ap.AddedAt ASC, p.PackageCode ASC;
    `);

  const dockerRows = componentResult.recordset.filter((row) => row.PackageType === 'docker');
  const inferredDockerPorts = dockerRows.length === 1 ? inferDockerPortsFromOpenUrl(appRow.OpenUrl) : [];

  const components = componentResult.recordset.map((row) => {
    const installOrder = Number(row.InstallOrder || 10);

    if (row.PackageType === 'apt') {
      return {
        componentId: row.PackageCode,
        type: 'apt',
        installOrder,
        required: true,
        packageName: row.PackageCode,
        version: row.Version || undefined
      };
    }

    if (row.PackageType === 'docker') {
      return {
        componentId: row.PackageCode,
        type: 'docker',
        installOrder,
        required: true,
        image: row.DockerImage || '',
        tag: row.Version || 'latest',
        containerName: row.PackageCode,
        ports: inferredDockerPorts
      };
    }

    return {
      componentId: row.PackageCode,
      type: 'deb',
      installOrder,
      required: true,
      packageName: row.PackageCode,
      version: row.Version || '',
      downloadUrl: `${String(baseUrl || '').replace(/\/+$/, '')}/api/package-versions/${encodeURIComponent(String(row.PackageVersionId))}/download`,
      sha256: row.FileChecksumSha256 || ''
    };
  });

  return {
    schemaVersion: '1.0',
    appId: String(appRow.Id),
    appName: appRow.AppName,
    version: appRow.AppVersion,
    openUrl: appRow.OpenUrl || '',
    architecture: 'amd64',
    components
  };
}

async function getStats() {
  const pool = await getPool();
  const result = await pool.request().query(`
    SELECT
      (SELECT COUNT_BIG(*) FROM dbo.Packages) AS TotalPackages,
      (SELECT COUNT_BIG(*) FROM dbo.Packages WHERE IsActive = 1) AS ActivePackages,
      (SELECT COUNT_BIG(*) FROM dbo.PackageVersions) AS TotalVersions,
      (SELECT COUNT_BIG(*) FROM dbo.Applications) AS TotalApplications,
      (SELECT COUNT_BIG(*) FROM dbo.Applications WHERE Status = N'Released') AS ReleasedApplications;
  `);

  const row = result.recordset[0];

  return {
    totalPackages: Number(row.TotalPackages || 0),
    activePackages: Number(row.ActivePackages || 0),
    totalVersions: Number(row.TotalVersions || 0),
    totalApplications: Number(row.TotalApplications || 0),
    releasedApplications: Number(row.ReleasedApplications || 0)
  };
}

async function getActivity() {
  const pool = await getPool();
  const result = await pool.request().query(`
    SELECT TOP (6)
      p.PackageName,
      p.PackageCode,
      pv.Version,
      pv.UploadedAt,
      pv.IsLatest
    FROM dbo.PackageVersions AS pv
    INNER JOIN dbo.Packages AS p
      ON p.Id = pv.PackageId
    ORDER BY pv.UploadedAt DESC;
  `);

  return result.recordset.map((row) => ({
    title: `${row.IsLatest ? 'Latest' : 'Upload'} ${row.Version}`,
    detail: `${row.PackageName} (${row.PackageCode})`,
    time: formatDate(row.UploadedAt),
    icon: row.IsLatest ? 'stars' : 'upload_file'
  }));
}

async function getPackageVersionDownload(packageVersionId) {
  const pool = await getPool();
  const result = await pool.request()
    .input('Id', sql.NVarChar(100), String(packageVersionId || '').trim())
    .query(`
      SELECT TOP (1)
        pv.Id,
        pv.PackageId,
        pv.Version,
        pv.FilePath,
        pv.FileChecksumSha256,
        pv.FileSizeBytes,
        p.PackageCode,
        p.PackageName,
        p.PackageType
      FROM dbo.PackageVersions AS pv
      INNER JOIN dbo.Packages AS p
        ON p.Id = pv.PackageId
      WHERE CONVERT(NVARCHAR(36), pv.Id) = @Id;
    `);

  const row = result.recordset[0];
  if (!row) return null;

  return {
    id: String(row.Id),
    packageId: String(row.PackageId),
    packageCode: row.PackageCode,
    packageName: row.PackageName,
    packageType: row.PackageType,
    version: row.Version,
    filePath: row.FilePath || '',
    checksum: row.FileChecksumSha256 || '',
    fileSizeBytes: Number(row.FileSizeBytes || 0)
  };
}

async function createPackageWithVersion(input) {
  await ensurePackageTypeSchema();
  const pool = await getPool();
  const transaction = new sql.Transaction(pool);

  await transaction.begin();

  try {
    const packageResult = await new sql.Request(transaction)
      .input('PackageCode', sql.NVarChar(100), input.packageCode)
      .input('PackageName', sql.NVarChar(200), input.packageName)
      .input('PackageType', sql.NVarChar(20), input.packageType)
      .input('Description', sql.NVarChar(sql.MAX), input.description || null)
      .input('CreatedByUserId', sql.UniqueIdentifier, input.createdByUserId)
      .query(`
        INSERT dbo.Packages (PackageCode, PackageName, PackageType, Description, CreatedByUserId)
        OUTPUT inserted.Id
        VALUES (@PackageCode, @PackageName, @PackageType, @Description, @CreatedByUserId);
      `);

    const packageId = String(packageResult.recordset[0].Id);
    const versionResult = await new sql.Request(transaction)
      .input('PackageId', sql.UniqueIdentifier, packageId)
      .input('Version', sql.NVarChar(50), input.version)
      .input('FilePath', sql.NVarChar(1000), input.filePath || null)
      .input('DockerImage', sql.NVarChar(500), input.dockerImage || null)
      .input('FileChecksumSha256', sql.Char(64), input.checksum || null)
      .input('FileSizeBytes', sql.BigInt, input.fileSizeBytes || null)
      .input('ChangeLog', sql.NVarChar(sql.MAX), input.changeLog || null)
      .input('ReleaseDate', sql.DateTime2, toSqlDateTime2Date(input.releaseDate))
      .query(`
        INSERT dbo.PackageVersions (
          PackageId, Version, FilePath, DockerImage, FileChecksumSha256,
          FileSizeBytes, ChangeLog, ReleaseDate, IsLatest
        )
        OUTPUT inserted.Id
        VALUES (
          @PackageId, @Version, @FilePath, @DockerImage, @FileChecksumSha256,
          @FileSizeBytes, @ChangeLog, @ReleaseDate, 1
        );
      `);

    await transaction.commit();

    return {
      packageId,
      versionId: String(versionResult.recordset[0].Id)
    };
  } catch (error) {
    await transaction.rollback();
    if (error.number === 2601 || error.number === 2627) {
      throw duplicatePackageError();
    }

    throw error;
  }
}

async function addPackageVersion(input) {
  const pool = await getPool();
  let insertResult;

  try {
    insertResult = await pool.request()
      .input('PackageId', sql.UniqueIdentifier, input.packageId)
      .input('Version', sql.NVarChar(50), input.version)
      .input('FilePath', sql.NVarChar(1000), input.filePath || null)
      .input('DockerImage', sql.NVarChar(500), input.dockerImage || null)
      .input('FileChecksumSha256', sql.Char(64), input.checksum || null)
      .input('FileSizeBytes', sql.BigInt, input.fileSizeBytes || null)
      .input('ChangeLog', sql.NVarChar(sql.MAX), input.changeLog || null)
      .input('ReleaseDate', sql.DateTime2, toSqlDateTime2Date(input.releaseDate))
      .query(`
        INSERT dbo.PackageVersions (
          PackageId, Version, FilePath, DockerImage, FileChecksumSha256,
          FileSizeBytes, ChangeLog, ReleaseDate
        )
        OUTPUT inserted.Id
        VALUES (
          @PackageId, @Version, @FilePath, @DockerImage, @FileChecksumSha256,
          @FileSizeBytes, @ChangeLog, @ReleaseDate
        );
      `);
  } catch (error) {
    if (error.number === 2601 || error.number === 2627) {
      throw duplicatePackageError();
    }

    throw error;
  }

  const versionId = String(insertResult.recordset[0].Id);
  await pool.request()
    .input('PackageVersionId', sql.UniqueIdentifier, versionId)
    .execute('dbo.SetLatestPackageVersion');

  return versionId;
}

async function replacePackageVersionArtifact(input) {
  const pool = await getPool();
  const result = await pool.request()
    .input('PackageId', sql.UniqueIdentifier, input.packageId)
    .input('Version', sql.NVarChar(50), input.version)
    .input('FilePath', sql.NVarChar(1000), input.filePath || null)
    .input('DockerImage', sql.NVarChar(500), input.dockerImage || null)
    .input('FileChecksumSha256', sql.Char(64), input.checksum || null)
    .input('FileSizeBytes', sql.BigInt, input.fileSizeBytes || null)
    .input('ChangeLog', sql.NVarChar(sql.MAX), input.changeLog || null)
    .input('ReleaseDate', sql.DateTime2, toSqlDateTime2Date(input.releaseDate))
    .query(`
      UPDATE dbo.PackageVersions
      SET FilePath = @FilePath,
          DockerImage = @DockerImage,
          FileChecksumSha256 = @FileChecksumSha256,
          FileSizeBytes = @FileSizeBytes,
          ChangeLog = COALESCE(@ChangeLog, ChangeLog),
          ReleaseDate = @ReleaseDate,
          UploadedAt = SYSUTCDATETIME(),
          IsDeprecated = 0
      OUTPUT inserted.Id
      WHERE PackageId = @PackageId
        AND Version = @Version;
    `);

  const row = result.recordset[0];
  if (!row) return null;

  const versionId = String(row.Id);
  await pool.request()
    .input('PackageVersionId', sql.UniqueIdentifier, versionId)
    .execute('dbo.SetLatestPackageVersion');

  return versionId;
}

async function deletePackage(packageId) {
  const pool = await getPool();
  const transaction = new sql.Transaction(pool);

  await transaction.begin();

  try {
    await new sql.Request(transaction)
      .input('Id', sql.UniqueIdentifier, packageId)
      .query(`
        DELETE FROM dbo.ApplicationPackages
        WHERE PackageId = @Id;
      `);

    const result = await new sql.Request(transaction)
      .input('Id', sql.UniqueIdentifier, packageId)
      .query(`
        DELETE FROM dbo.Packages
        OUTPUT deleted.Id
        WHERE Id = @Id;
      `);

    await transaction.commit();
    return result.recordset.length > 0;
  } catch (error) {
    await transaction.rollback();
    throw error;
  }
}

async function setLatestPackageVersion(packageVersionId) {
  const pool = await getPool();
  await pool.request()
    .input('PackageVersionId', sql.UniqueIdentifier, packageVersionId)
    .execute('dbo.SetLatestPackageVersion');
}

async function deletePackageVersion(packageVersionId) {
  const pool = await getPool();
  const versionResult = await pool.request()
    .input('Id', sql.UniqueIdentifier, packageVersionId)
    .query(`
      SELECT TOP (1) PackageId
      FROM dbo.PackageVersions
      WHERE Id = @Id;
    `);

  const version = versionResult.recordset[0];
  if (!version) {
    return {
      deleted: false,
      packageId: null
    };
  }

  await pool.request()
    .input('PackageVersionId', sql.UniqueIdentifier, packageVersionId)
    .execute('dbo.DeletePackageVersion');

  return {
    deleted: true,
    packageId: String(version.PackageId)
  };
}

async function createApplication(input) {
  await ensureApplicationOpenUrlSchema();
  const pool = await getPool();
  const transaction = new sql.Transaction(pool);

  await transaction.begin();

  try {
    const appResult = await new sql.Request(transaction)
      .input('AppCode', sql.NVarChar(100), input.appCode)
      .input('AppName', sql.NVarChar(200), input.appName)
      .input('AppVersion', sql.NVarChar(50), input.appVersion)
      .input('Notes', sql.NVarChar(500), input.notes || null)
      .input('OpenUrl', sql.NVarChar(500), input.openUrl || null)
      .input('CreatedByUserId', sql.UniqueIdentifier, input.createdByUserId)
      .input('Status', sql.NVarChar(50), normalizeApplicationStatus(input.status))
      .query(`
        INSERT dbo.Applications (AppCode, AppName, AppVersion, Notes, OpenUrl, CreatedByUserId, Status)
        OUTPUT inserted.Id
        VALUES (@AppCode, @AppName, @AppVersion, @Notes, @OpenUrl, @CreatedByUserId, @Status);
      `);

    const applicationId = String(appResult.recordset[0].Id);

    for (const packageItem of input.packages) {
      await new sql.Request(transaction)
        .input('ApplicationId', sql.UniqueIdentifier, applicationId)
        .input('PackageId', sql.UniqueIdentifier, packageItem.packageId)
        .input('SelectedVersionId', sql.UniqueIdentifier, packageItem.selectedVersionId || null)
        .query(`
          INSERT dbo.ApplicationPackages (ApplicationId, PackageId, SelectedVersionId)
          VALUES (@ApplicationId, @PackageId, @SelectedVersionId);
        `);
    }

    await transaction.commit();
    return applicationId;
  } catch (error) {
    await transaction.rollback();
    if (error.number === 2601 || error.number === 2627) {
      throw duplicateApplicationError();
    }

    throw error;
  }
}

async function updateApplication(input) {
  await ensureApplicationOpenUrlSchema();
  const pool = await getPool();
  const transaction = new sql.Transaction(pool);

  await transaction.begin();

  try {
    const conflictResult = await new sql.Request(transaction)
      .input('ApplicationId', sql.UniqueIdentifier, input.applicationId)
      .input('AppCode', sql.NVarChar(100), input.appCode)
      .query(`
        SELECT TOP (1) Id
        FROM dbo.Applications
        WHERE AppCode = @AppCode
          AND Id <> @ApplicationId;
      `);

    if (conflictResult.recordset.length > 0) {
      throw duplicateApplicationError();
    }

    const appResult = await new sql.Request(transaction)
      .input('ApplicationId', sql.UniqueIdentifier, input.applicationId)
      .input('AppCode', sql.NVarChar(100), input.appCode)
      .input('AppName', sql.NVarChar(200), input.appName)
      .input('AppVersion', sql.NVarChar(50), input.appVersion)
      .input('Notes', sql.NVarChar(500), input.notes || null)
      .input('OpenUrl', sql.NVarChar(500), input.openUrl || null)
      .input('Status', sql.NVarChar(50), normalizeApplicationStatus(input.status))
      .query(`
        UPDATE dbo.Applications
        SET AppCode = @AppCode,
            AppName = @AppName,
            AppVersion = @AppVersion,
            Notes = @Notes,
            OpenUrl = @OpenUrl,
            Status = @Status,
            UpdatedAt = SYSUTCDATETIME()
        OUTPUT inserted.Id
        WHERE Id = @ApplicationId;
      `);

    if (appResult.recordset.length === 0) {
      await transaction.commit();
      return null;
    }

    await new sql.Request(transaction)
      .input('ApplicationId', sql.UniqueIdentifier, input.applicationId)
      .query(`
        DELETE FROM dbo.ApplicationPackages
        WHERE ApplicationId = @ApplicationId;
      `);

    for (const packageItem of input.packages) {
      await new sql.Request(transaction)
        .input('ApplicationId', sql.UniqueIdentifier, input.applicationId)
        .input('PackageId', sql.UniqueIdentifier, packageItem.packageId)
        .input('SelectedVersionId', sql.UniqueIdentifier, packageItem.selectedVersionId || null)
        .query(`
          INSERT dbo.ApplicationPackages (ApplicationId, PackageId, SelectedVersionId)
          VALUES (@ApplicationId, @PackageId, @SelectedVersionId);
        `);
    }

    await transaction.commit();
  } catch (error) {
    await transaction.rollback();
    if (error.code === 'DUPLICATE_APPLICATION') {
      throw error;
    }

    if (error.number === 2601 || error.number === 2627) {
      throw duplicateApplicationError();
    }

    throw error;
  }

  return getApplicationById(input.applicationId);
}

async function updateApplicationStatus(applicationId, status) {
  const pool = await getPool();
  const result = await pool.request()
    .input('ApplicationId', sql.UniqueIdentifier, applicationId)
    .input('Status', sql.NVarChar(50), normalizeApplicationStatus(status))
    .query(`
      UPDATE dbo.Applications
      SET Status = @Status,
          UpdatedAt = SYSUTCDATETIME()
      OUTPUT inserted.Id
      WHERE Id = @ApplicationId;
    `);

  return result.recordset.length > 0;
}

async function deleteApplication(applicationId) {
  const pool = await getPool();
  const result = await pool.request()
    .input('ApplicationId', sql.UniqueIdentifier, applicationId)
    .query(`
      DELETE FROM dbo.Applications
      OUTPUT deleted.Id
      WHERE Id = @ApplicationId;
    `);

  return result.recordset.length > 0;
}

async function removeApplicationPackage(applicationId, packageId) {
  const pool = await getPool();
  const result = await pool.request()
    .input('ApplicationId', sql.UniqueIdentifier, applicationId)
    .input('PackageId', sql.UniqueIdentifier, packageId)
    .query(`
      DELETE FROM dbo.ApplicationPackages
      OUTPUT deleted.Id
      WHERE ApplicationId = @ApplicationId
        AND PackageId = @PackageId;
    `);

  return result.recordset.length > 0;
}

async function listDocuments() {
  await ensureDocumentSchema();
  const pool = await getPool();
  const result = await pool.request().query(`
    SELECT
      d.Id,
      d.Title,
      d.Category,
      d.Summary,
      CASE WHEN NULLIF(LTRIM(RTRIM(d.Content)), N'') IS NULL THEN 0 ELSE 1 END AS HasContent,
      d.FilePath,
      d.OriginalFileName,
      d.MimeType,
      d.FileSizeBytes,
      d.CreatedByUserId,
      d.CreatedAt,
      d.UpdatedAt,
      u.Username AS CreatedByUsername
    FROM dbo.Documents AS d
    INNER JOIN dbo.Users AS u
      ON u.Id = d.CreatedByUserId
    ORDER BY COALESCE(d.UpdatedAt, d.CreatedAt) DESC, d.Title ASC;
  `);

  return result.recordset.map(mapDocumentRow);
}

async function getDocumentById(documentId) {
  await ensureDocumentSchema();
  const pool = await getPool();
  const result = await pool.request()
    .input('Id', sql.NVarChar(100), String(documentId || '').trim())
    .query(`
      SELECT TOP (1)
        d.Id,
        d.Title,
        d.Category,
        d.Summary,
        d.Content,
        d.FilePath,
        d.OriginalFileName,
        d.MimeType,
        d.FileSizeBytes,
        d.CreatedByUserId,
        d.CreatedAt,
        d.UpdatedAt,
        u.Username AS CreatedByUsername
      FROM dbo.Documents AS d
      INNER JOIN dbo.Users AS u
        ON u.Id = d.CreatedByUserId
      WHERE CONVERT(NVARCHAR(36), d.Id) = @Id;
    `);

  return mapDocumentRow(result.recordset[0]);
}

async function createDocument(input) {
  await ensureDocumentSchema();
  const pool = await getPool();
  const result = await pool.request()
    .input('Title', sql.NVarChar(200), String(input.title || '').trim())
    .input('Category', sql.NVarChar(50), input.category)
    .input('Summary', sql.NVarChar(1000), String(input.summary || '').trim() || null)
    .input('Content', sql.NVarChar(sql.MAX), String(input.content || '').trim() || null)
    .input('FilePath', sql.NVarChar(1000), input.filePath || null)
    .input('OriginalFileName', sql.NVarChar(260), input.originalFileName || null)
    .input('MimeType', sql.NVarChar(200), input.mimeType || null)
    .input('FileSizeBytes', sql.BigInt, input.fileSizeBytes ?? null)
    .input('CreatedByUserId', sql.UniqueIdentifier, input.createdByUserId)
    .query(`
      INSERT dbo.Documents
      (
        Title,
        Category,
        Summary,
        Content,
        FilePath,
        OriginalFileName,
        MimeType,
        FileSizeBytes,
        CreatedByUserId
      )
      OUTPUT inserted.Id
      VALUES
      (
        @Title,
        @Category,
        @Summary,
        @Content,
        @FilePath,
        @OriginalFileName,
        @MimeType,
        @FileSizeBytes,
        @CreatedByUserId
      );
    `);

  return String(result.recordset[0].Id);
}

async function updateDocument(input) {
  await ensureDocumentSchema();
  const pool = await getPool();
  const replaceAttachment = Boolean(input.replaceAttachment);
  const result = await pool.request()
    .input('Id', sql.UniqueIdentifier, input.documentId)
    .input('Title', sql.NVarChar(200), String(input.title || '').trim())
    .input('Category', sql.NVarChar(50), input.category)
    .input('Summary', sql.NVarChar(1000), String(input.summary || '').trim() || null)
    .input('Content', sql.NVarChar(sql.MAX), String(input.content || '').trim() || null)
    .input('ReplaceAttachment', sql.Bit, replaceAttachment ? 1 : 0)
    .input('FilePath', sql.NVarChar(1000), input.filePath || null)
    .input('OriginalFileName', sql.NVarChar(260), input.originalFileName || null)
    .input('MimeType', sql.NVarChar(200), input.mimeType || null)
    .input('FileSizeBytes', sql.BigInt, input.fileSizeBytes ?? null)
    .query(`
      UPDATE dbo.Documents
      SET Title = @Title,
          Category = @Category,
          Summary = @Summary,
          Content = @Content,
          FilePath = CASE WHEN @ReplaceAttachment = 1 THEN @FilePath ELSE FilePath END,
          OriginalFileName = CASE WHEN @ReplaceAttachment = 1 THEN @OriginalFileName ELSE OriginalFileName END,
          MimeType = CASE WHEN @ReplaceAttachment = 1 THEN @MimeType ELSE MimeType END,
          FileSizeBytes = CASE WHEN @ReplaceAttachment = 1 THEN @FileSizeBytes ELSE FileSizeBytes END,
          UpdatedAt = SYSUTCDATETIME()
      OUTPUT
        inserted.Id,
        deleted.FilePath AS PreviousFilePath,
        inserted.FilePath AS CurrentFilePath
      WHERE Id = @Id;
    `);
  const row = result.recordset[0];

  if (!row) return null;

  return {
    id: String(row.Id),
    previousFilePath: row.PreviousFilePath || '',
    currentFilePath: row.CurrentFilePath || '',
    attachmentChanged: replaceAttachment
  };
}

async function deleteDocument(documentId) {
  await ensureDocumentSchema();
  const pool = await getPool();
  const result = await pool.request()
    .input('Id', sql.UniqueIdentifier, documentId)
    .query(`
      DELETE FROM dbo.Documents
      OUTPUT
        deleted.Id,
        deleted.FilePath,
        deleted.OriginalFileName
      WHERE Id = @Id;
    `);
  const row = result.recordset[0];

  if (!row) return null;

  return {
    id: String(row.Id),
    filePath: row.FilePath || '',
    originalFileName: row.OriginalFileName || ''
  };
}

async function getPageData(currentUser) {
  const [stats, packageRows, applications, activity] = await Promise.all([
    getStats(),
    listPackages(),
    listApplications(),
    getActivity()
  ]);
  const packages = await attachPackageVersions(packageRows);

  return {
    currentUser,
    stats,
    packages,
    applications,
    activity
  };
}

module.exports = {
  authenticateUser,
  confirmEmailToken,
  createEmailConfirmationToken,
  createUser,
  deleteUser,
  getUserById,
  getPendingUserByEmail,
  getRegistrationConflict,
  listUsers,
  updateUser,
  updateUserAccess,
  getPageData,
  listPackages,
  listApplications,
  getApplicationManifest,
  getPackageVersionDownload,
  getPackageById,
  getApplicationById,
  createPackageWithVersion,
  addPackageVersion,
  replacePackageVersionArtifact,
  deletePackage,
  setLatestPackageVersion,
  deletePackageVersion,
  createApplication,
  updateApplication,
  updateApplicationStatus,
  deleteApplication,
  removeApplicationPackage,
  listDocuments,
  getDocumentById,
  createDocument,
  updateDocument,
  deleteDocument
};
