const crypto = require('crypto');
const { sql, getPool } = require('./db');

const SCHEMA_CHECK_TTL_MS = 30 * 1000;
const ALLOWED_SEVERITIES = new Set(['info', 'success', 'warning', 'error']);
let notificationSchemaState = {
  available: false,
  checkedAt: 0
};

function normalizeSeverity(value) {
  const severity = String(value || '').trim().toLowerCase();
  return ALLOWED_SEVERITIES.has(severity) ? severity : 'info';
}

function toIsoString(value) {
  if (!value) return null;
  const date = value instanceof Date ? value : new Date(value);
  return Number.isNaN(date.getTime()) ? null : date.toISOString();
}

function mapNotificationRow(row) {
  return {
    id: String(row.Id),
    eventType: row.EventType,
    isSystem: row.EventType === 'system.announcement',
    severity: normalizeSeverity(row.Severity),
    title: row.Title,
    message: row.Message,
    entityType: row.EntityType || '',
    entityId: row.EntityId || '',
    actionUrl: row.ActionUrl || '',
    actorName: row.ActorName || '',
    isRead: Boolean(row.ReadAt),
    readAt: toIsoString(row.ReadAt),
    createdAt: toIsoString(row.CreatedAt)
  };
}

async function checkNotificationSchema(force = false) {
  const now = Date.now();
  if (!force && now - notificationSchemaState.checkedAt < SCHEMA_CHECK_TTL_MS) {
    return notificationSchemaState.available;
  }

  const pool = await getPool();
  const result = await pool.request().query(`
    SELECT CASE WHEN OBJECT_ID(N'dbo.Notifications', N'U') IS NULL THEN 0 ELSE 1 END AS IsAvailable;
  `);

  notificationSchemaState = {
    available: Boolean(result.recordset[0] && result.recordset[0].IsAvailable),
    checkedAt: now
  };
  return notificationSchemaState.available;
}

async function listNotifications(userId, options = {}) {
  const available = await checkNotificationSchema();
  if (!available) {
    return {
      available: false,
      notifications: [],
      unreadCount: 0
    };
  }

  const limit = Math.min(Math.max(Math.trunc(Number(options.limit)) || 20, 1), 50);
  const unreadOnly = Boolean(options.unreadOnly);
  const pool = await getPool();
  const result = await pool.request()
    .input('RecipientUserId', sql.UniqueIdentifier, userId)
    .input('Limit', sql.Int, limit)
    .input('UnreadOnly', sql.Bit, unreadOnly ? 1 : 0)
    .query(`
      SELECT COUNT_BIG(*) AS UnreadCount
      FROM dbo.Notifications
      WHERE RecipientUserId = @RecipientUserId
        AND ReadAt IS NULL
        AND (ExpiresAt IS NULL OR ExpiresAt > SYSUTCDATETIME());

      SELECT TOP (@Limit)
        Id, EventType, Severity, Title, Message, EntityType, EntityId,
        ActionUrl, ActorName, ReadAt, CreatedAt
      FROM dbo.Notifications
      WHERE RecipientUserId = @RecipientUserId
        AND (@UnreadOnly = 0 OR ReadAt IS NULL)
        AND (ExpiresAt IS NULL OR ExpiresAt > SYSUTCDATETIME())
      ORDER BY CreatedAt DESC, Id DESC;
    `);

  return {
    available: true,
    unreadCount: Number(result.recordsets[0][0].UnreadCount || 0),
    notifications: result.recordsets[1].map(mapNotificationRow)
  };
}

async function getUnreadCount(userId) {
  const available = await checkNotificationSchema();
  if (!available) {
    return { available: false, unreadCount: 0 };
  }

  const pool = await getPool();
  const result = await pool.request()
    .input('RecipientUserId', sql.UniqueIdentifier, userId)
    .query(`
      SELECT COUNT_BIG(*) AS UnreadCount
      FROM dbo.Notifications
      WHERE RecipientUserId = @RecipientUserId
        AND ReadAt IS NULL
        AND (ExpiresAt IS NULL OR ExpiresAt > SYSUTCDATETIME());
    `);

  return {
    available: true,
    unreadCount: Number(result.recordset[0].UnreadCount || 0)
  };
}

async function markNotificationRead(notificationId, userId) {
  if (!await checkNotificationSchema()) {
    return { available: false, updated: false };
  }

  const pool = await getPool();
  const result = await pool.request()
    .input('Id', sql.UniqueIdentifier, notificationId)
    .input('RecipientUserId', sql.UniqueIdentifier, userId)
    .query(`
      UPDATE dbo.Notifications
      SET ReadAt = COALESCE(ReadAt, SYSUTCDATETIME())
      OUTPUT inserted.Id
      WHERE Id = @Id
        AND RecipientUserId = @RecipientUserId;
    `);

  return {
    available: true,
    updated: result.recordset.length > 0
  };
}

async function markAllNotificationsRead(userId) {
  if (!await checkNotificationSchema()) {
    return { available: false, updatedCount: 0 };
  }

  const pool = await getPool();
  const result = await pool.request()
    .input('RecipientUserId', sql.UniqueIdentifier, userId)
    .query(`
      UPDATE dbo.Notifications
      SET ReadAt = SYSUTCDATETIME()
      OUTPUT inserted.Id
      WHERE RecipientUserId = @RecipientUserId
        AND ReadAt IS NULL
        AND (ExpiresAt IS NULL OR ExpiresAt > SYSUTCDATETIME());
    `);

  return {
    available: true,
    updatedCount: result.recordset.length
  };
}

async function createSystemBroadcast(input) {
  if (!await checkNotificationSchema()) {
    return { available: false, broadcastId: null, recipientCount: 0 };
  }

  const pool = await getPool();
  const broadcastId = crypto.randomUUID();
  const result = await pool.request()
    .input('ActorUserId', sql.UniqueIdentifier, input.actorUserId)
    .input('ActorName', sql.NVarChar(200), input.actorName || null)
    .input('BroadcastId', sql.UniqueIdentifier, broadcastId)
    .input('Severity', sql.NVarChar(20), normalizeSeverity(input.severity))
    .input('Title', sql.NVarChar(200), input.title)
    .input('Message', sql.NVarChar(1000), input.message)
    .input('ActionUrl', sql.NVarChar(1000), input.actionUrl || null)
    .query(`
      INSERT dbo.Notifications (
        RecipientUserId, ActorUserId, ActorName, BroadcastId, EventType,
        Severity, Title, Message, EntityType, ActionUrl
      )
      SELECT
        u.Id, @ActorUserId, @ActorName, @BroadcastId, N'system.announcement',
        @Severity, @Title, @Message, N'system', @ActionUrl
      FROM dbo.Users AS u
      WHERE u.IsActive = 1;

      DECLARE @RecipientCount INT = @@ROWCOUNT;
      SELECT @RecipientCount AS RecipientCount;
    `);

  return {
    available: true,
    broadcastId,
    recipientCount: Number(result.recordset[0].RecipientCount || 0)
  };
}

async function createNotificationsForUsers(input) {
  if (!await checkNotificationSchema()) {
    return { available: false, recipientCount: 0 };
  }

  const uniqueRecipientIds = Array.from(new Set((input.recipientUserIds || []).filter(Boolean))).slice(0, 1000);
  if (uniqueRecipientIds.length === 0) {
    return { available: true, recipientCount: 0 };
  }

  const pool = await getPool();
  const request = pool.request()
    .input('ActorUserId', sql.UniqueIdentifier, input.actorUserId || null)
    .input('ActorName', sql.NVarChar(200), input.actorName || null)
    .input('EventType', sql.NVarChar(100), input.eventType)
    .input('Severity', sql.NVarChar(20), normalizeSeverity(input.severity))
    .input('Title', sql.NVarChar(200), input.title)
    .input('Message', sql.NVarChar(1000), input.message)
    .input('EntityType', sql.NVarChar(50), input.entityType || null)
    .input('EntityId', sql.NVarChar(100), input.entityId || null)
    .input('ActionUrl', sql.NVarChar(1000), input.actionUrl || null);
  const recipientRows = uniqueRecipientIds.map((userId, index) => {
    const parameterName = `RecipientUserId${index}`;
    request.input(parameterName, sql.UniqueIdentifier, userId);
    return `SELECT @${parameterName} AS Id`;
  }).join('\nUNION ALL\n');

  const result = await request.query(`
    WITH RequestedRecipients AS (
      ${recipientRows}
    )
    INSERT dbo.Notifications (
      RecipientUserId, ActorUserId, ActorName, EventType, Severity,
      Title, Message, EntityType, EntityId, ActionUrl
    )
    SELECT DISTINCT
      u.Id, @ActorUserId, @ActorName, @EventType, @Severity,
      @Title, @Message, @EntityType, @EntityId, @ActionUrl
    FROM dbo.Users AS u
    INNER JOIN RequestedRecipients AS requested ON requested.Id = u.Id
    WHERE u.IsActive = 1
      AND (@ActorUserId IS NULL OR u.Id <> @ActorUserId);

    DECLARE @RecipientCount INT = @@ROWCOUNT;
    SELECT @RecipientCount AS RecipientCount;
  `);

  return {
    available: true,
    recipientCount: Number(result.recordset[0].RecipientCount || 0)
  };
}

async function getPackageVersionAudience(packageId) {
  if (!await checkNotificationSchema()) return [];

  const pool = await getPool();
  const result = await pool.request()
    .input('PackageId', sql.UniqueIdentifier, packageId)
    .query(`
      SELECT DISTINCT a.CreatedByUserId AS UserId
      FROM dbo.ApplicationPackages AS ap
      INNER JOIN dbo.Applications AS a ON a.Id = ap.ApplicationId
      WHERE ap.PackageId = @PackageId;
    `);

  return result.recordset.map((row) => String(row.UserId));
}

async function notifyPackageVersionAvailable(input) {
  if (!await checkNotificationSchema()) {
    return { available: false, recipientCount: 0 };
  }

  const pool = await getPool();
  const packageResult = await pool.request()
    .input('PackageId', sql.UniqueIdentifier, input.packageId)
    .query(`
      SELECT TOP (1) Id, PackageCode, PackageName
      FROM dbo.Packages
      WHERE Id = @PackageId;
    `);
  const packageRow = packageResult.recordset[0];
  if (!packageRow) {
    return { available: true, recipientCount: 0 };
  }

  const recipientUserIds = await getPackageVersionAudience(input.packageId);
  const isArtifactUpdate = Boolean(input.isArtifactUpdate);
  return createNotificationsForUsers({
    recipientUserIds,
    actorUserId: input.actorUserId,
    actorName: input.actorName,
    eventType: isArtifactUpdate ? 'package.version_artifact_updated' : 'package.version_available',
    severity: 'info',
    title: isArtifactUpdate
      ? `Artifact ${packageRow.PackageCode} ${input.version} đã được cập nhật`
      : `Package ${packageRow.PackageCode} có version ${input.version} mới`,
    message: isArtifactUpdate
      ? 'Artifact của version hiện tại đã được upload lại. Hãy kiểm tra trước khi triển khai tiếp.'
      : 'Application của bạn đang sử dụng package này. Hãy kiểm tra và chọn version mới khi phù hợp.',
    entityType: 'package',
    entityId: String(packageRow.Id),
    actionUrl: `/packages/${packageRow.Id}`
  });
}

async function getPackageDeletionContext(packageId) {
  if (!await checkNotificationSchema()) return null;

  const pool = await getPool();
  const result = await pool.request()
    .input('PackageId', sql.UniqueIdentifier, packageId)
    .query(`
      SELECT TOP (1) Id, PackageCode, PackageName, CreatedByUserId
      FROM dbo.Packages
      WHERE Id = @PackageId;

      SELECT DISTINCT recipients.UserId
      FROM (
        SELECT a.CreatedByUserId AS UserId
        FROM dbo.ApplicationPackages AS ap
        INNER JOIN dbo.Applications AS a ON a.Id = ap.ApplicationId
        WHERE ap.PackageId = @PackageId
        UNION
        SELECT CreatedByUserId FROM dbo.Packages WHERE Id = @PackageId
        UNION
        SELECT Id FROM dbo.Users WHERE Role = N'Admin'
      ) AS recipients;
    `);
  const packageRow = result.recordsets[0][0];
  if (!packageRow) return null;

  return {
    packageId: String(packageRow.Id),
    packageCode: packageRow.PackageCode,
    packageName: packageRow.PackageName,
    recipientUserIds: result.recordsets[1].map((row) => String(row.UserId))
  };
}

async function getPackageVersionDeletionContext(packageVersionId) {
  if (!await checkNotificationSchema()) return null;

  const pool = await getPool();
  const result = await pool.request()
    .input('PackageVersionId', sql.UniqueIdentifier, packageVersionId)
    .query(`
      SELECT TOP (1)
        pv.Id, pv.PackageId, pv.Version, p.PackageCode, p.PackageName, p.CreatedByUserId
      FROM dbo.PackageVersions AS pv
      INNER JOIN dbo.Packages AS p ON p.Id = pv.PackageId
      WHERE pv.Id = @PackageVersionId;

      SELECT DISTINCT recipients.UserId
      FROM (
        SELECT a.CreatedByUserId AS UserId
        FROM dbo.ApplicationPackages AS ap
        INNER JOIN dbo.Applications AS a ON a.Id = ap.ApplicationId
        WHERE ap.SelectedVersionId = @PackageVersionId
        UNION
        SELECT p.CreatedByUserId
        FROM dbo.PackageVersions AS pv
        INNER JOIN dbo.Packages AS p ON p.Id = pv.PackageId
        WHERE pv.Id = @PackageVersionId
        UNION
        SELECT Id FROM dbo.Users WHERE Role = N'Admin'
      ) AS recipients;
    `);
  const versionRow = result.recordsets[0][0];
  if (!versionRow) return null;

  return {
    packageVersionId: String(versionRow.Id),
    packageId: String(versionRow.PackageId),
    packageCode: versionRow.PackageCode,
    packageName: versionRow.PackageName,
    version: versionRow.Version,
    recipientUserIds: result.recordsets[1].map((row) => String(row.UserId))
  };
}

async function getApplicationReleaseContext(applicationId) {
  if (!await checkNotificationSchema()) return null;

  const pool = await getPool();
  const result = await pool.request()
    .input('ApplicationId', sql.UniqueIdentifier, applicationId)
    .query(`
      SELECT TOP (1) Id, AppCode, AppName, AppVersion, CreatedByUserId
      FROM dbo.Applications
      WHERE Id = @ApplicationId;

      SELECT DISTINCT recipients.UserId
      FROM (
        SELECT CreatedByUserId AS UserId
        FROM dbo.Applications
        WHERE Id = @ApplicationId
        UNION
        SELECT Id FROM dbo.Users WHERE Role = N'Admin'
      ) AS recipients;
    `);
  const applicationRow = result.recordsets[0][0];
  if (!applicationRow) return null;

  return {
    applicationId: String(applicationRow.Id),
    appCode: applicationRow.AppCode,
    appName: applicationRow.AppName,
    appVersion: applicationRow.AppVersion,
    recipientUserIds: result.recordsets[1].map((row) => String(row.UserId))
  };
}

async function notifyApplicationReleased(input) {
  const context = await getApplicationReleaseContext(input.applicationId);
  if (!context) {
    return {
      available: await checkNotificationSchema(),
      recipientCount: 0
    };
  }

  return createNotificationsForUsers({
    recipientUserIds: context.recipientUserIds,
    actorUserId: input.actorUserId,
    actorName: input.actorName,
    eventType: 'application.released',
    severity: 'success',
    title: `Application ${context.appCode} đã được release`,
    message: `Version ${context.appVersion} đã chuyển sang trạng thái Released.`,
    entityType: 'application',
    entityId: context.applicationId,
    actionUrl: `/applications/${context.applicationId}`
  });
}

module.exports = {
  checkNotificationSchema,
  createNotificationsForUsers,
  createSystemBroadcast,
  getApplicationReleaseContext,
  getPackageImpact: getPackageDeletionContext,
  getPackageDeletionContext,
  getPackageVersionAudience,
  getPackageVersionImpact: getPackageVersionDeletionContext,
  getPackageVersionDeletionContext,
  getUnreadCount,
  listNotifications,
  markAllNotificationsRead,
  markNotificationRead: (userId, notificationId) => markNotificationRead(notificationId, userId),
  notifyApplicationReleased,
  notifyPackageVersionAvailable
};
