/**
 * Audit Service
 *
 * GDPR compliance:
 *   - Art. 5(2)  Accountability: every write to personal data is logged.
 *   - Art. 30    Records of Processing Activities: who, what, when, delta.
 *   - Art. 5(1)e Storage limitation: IP addresses anonymised after 90 days
 *                via anonymizeOldIpAddresses(), called by the scheduled job.
 *
 * Critical invariant: logAudit() MUST NEVER throw or reject.  Audit failures
 * are logged to Winston and silently swallowed so that the main request flow
 * is never interrupted.
 */

import pool from '../config/database';
import logger from '../utils/logger';
import notificationService from './notification.service';

// ---------------------------------------------------------------------------
// Enums — kept as const objects rather than TypeScript enums so that the
// values are available as plain strings in runtime code and SQL parameters.
// ---------------------------------------------------------------------------

export const AuditAction = {
  CREATE:           'CREATE',
  UPDATE:           'UPDATE',
  DELETE:           'DELETE',
  LOGIN:            'LOGIN',
  LOGOUT:           'LOGOUT',
  EXPORT:           'EXPORT',
  PERMISSION_DENIED:'PERMISSION_DENIED',
  PASSWORD_CHANGE:  'PASSWORD_CHANGE',
  ROLE_CHANGE:      'ROLE_CHANGE',
} as const;

export type AuditAction = typeof AuditAction[keyof typeof AuditAction];

export const AuditResourceType = {
  MEMBER:        'MEMBER',
  INCIDENT:      'INCIDENT',
  VEHICLE:       'VEHICLE',
  EQUIPMENT:     'EQUIPMENT',
  QUALIFICATION: 'QUALIFICATION',
  USER:          'USER',
  SYSTEM:        'SYSTEM',
} as const;

export type AuditResourceType = typeof AuditResourceType[keyof typeof AuditResourceType];

// ---------------------------------------------------------------------------
// Core interfaces
// ---------------------------------------------------------------------------

export interface AuditLogEntry {
  id:            string;          // UUID, set by database
  user_id:       string | null;   // UUID; null for unauthenticated events
  user_email:    string | null;   // denormalised snapshot
  action:        AuditAction;
  resource_type: AuditResourceType;
  resource_id:   string | null;
  old_value:     Record<string, unknown> | null;
  new_value:     Record<string, unknown> | null;
  ip_address:    string | null;
  user_agent:    string | null;
  metadata:      Record<string, unknown>;
  created_at:    Date;
}

/**
 * Input type for logAudit() — the caller never supplies id or created_at.
 */
export type AuditLogInput = Omit<AuditLogEntry, 'id' | 'created_at'>;

// ---------------------------------------------------------------------------
// Filter + pagination types (used by the admin API)
// ---------------------------------------------------------------------------

export interface AuditFilters {
  userId?:       string;
  action?:       AuditAction | AuditAction[];
  resourceType?: AuditResourceType | AuditResourceType[];
  resourceId?:   string;
  dateFrom?:     Date;
  dateTo?:       Date;
  page:          number;   // 1-based
  pageSize:      number;   // max 200
}

export interface AuditLogPage {
  entries: AuditLogEntry[];
  total:   number;
  page:    number;
  pages:   number;
}

// ---------------------------------------------------------------------------
// Sensitive field stripping
// Ensures that passwords, tokens, and secrets never appear in the log even
// if a caller accidentally passes a raw request body.
// ---------------------------------------------------------------------------

const SENSITIVE_KEYS = new Set([
  'password', 'password_hash', 'passwordHash',
  'secret', 'token', 'accessToken', 'refreshToken', 'access_token',
  'refresh_token', 'id_token', 'client_secret',
  'authorization', 'cookie',
]);

function stripSensitiveFields(
  value: Record<string, unknown> | null | undefined
): Record<string, unknown> | null {
  if (value === null || value === undefined) return null;

  const result: Record<string, unknown> = {};
  for (const [k, v] of Object.entries(value)) {
    if (SENSITIVE_KEYS.has(k.toLowerCase())) {
      result[k] = '[REDACTED]';
    } else if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
      result[k] = stripSensitiveFields(v as Record<string, unknown>);
    } else {
      result[k] = v;
    }
  }
  return result;
}

// ---------------------------------------------------------------------------
// AuditService
// ---------------------------------------------------------------------------

class AuditService {
  /**
   * logAudit — fire-and-forget.
   *
   * The caller does NOT await this.  Even if awaited, it will never reject;
   * all errors are swallowed and written to Winston instead.
   */
  async logAudit(entry: AuditLogInput): Promise<void> {
    // Start immediately — do not block the caller.
    this._writeAuditEntry(entry).catch(() => {
      // _writeAuditEntry already handles its own error logging; this .catch()
      // prevents a potential unhandledRejection if the async method itself
      // throws synchronously before the inner try/catch.
    });
  }

  /**
   * Internal write — all failures are caught and sent to Winston.
   * This method should never surface an exception to its caller.
   */
  private async _writeAuditEntry(entry: AuditLogInput): Promise<void> {
    try {
      const sanitisedOld = stripSensitiveFields(entry.old_value ?? null);
      const sanitisedNew = stripSensitiveFields(entry.new_value ?? null);

      const query = `
        INSERT INTO audit_log (
          user_id,
          user_email,
          action,
          resource_type,
          resource_id,
          old_value,
          new_value,
          ip_address,
          user_agent,
          metadata
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
      `;

      const values = [
        entry.user_id    ?? null,
        entry.user_email ?? null,
        entry.action,
        entry.resource_type,
        entry.resource_id ?? null,
        sanitisedOld ? JSON.stringify(sanitisedOld) : null,
        sanitisedNew ? JSON.stringify(sanitisedNew) : null,
        entry.ip_address ?? null,
        entry.user_agent ?? null,
        JSON.stringify(entry.metadata ?? {}),
      ];

      await pool.query(query, values);

      logger.debug('Audit entry written', {
        action:        entry.action,
        resource_type: entry.resource_type,
        resource_id:   entry.resource_id,
        user_id:       entry.user_id,
      });

      // Fire-and-forget — never block the audit log write
      this.alertAdminsIfSensitive(entry).catch(() => {});
    } catch (error) {
      // GDPR obligation: log the failure so it can be investigated, but
      // NEVER propagate — the main request must complete successfully.
      logger.error('AUDIT LOG FAILURE — entry could not be persisted to database', {
        error:         error instanceof Error ? error.message : String(error),
        action:        entry.action,
        resource_type: entry.resource_type,
        resource_id:   entry.resource_id,
        user_id:       entry.user_id,
        // Note: we intentionally do NOT log old_value/new_value here
        // because they may contain personal data.
      });
    }
  }

  // -------------------------------------------------------------------------
  // Audit alert notifications for admins
  // -------------------------------------------------------------------------

  /**
   * alertAdminsIfSensitive — checks whether an audit entry represents a
   * sensitive action and, if so, creates a notification for every active
   * admin user (except the actor themselves).
   *
   * This method MUST NEVER throw.  All errors are caught and logged.
   */
  async alertAdminsIfSensitive(entry: AuditLogInput): Promise<void> {
    try {
      const sensitiveActions: Record<string, string> = {
        'PERMISSION_DENIED': 'Zugriff verweigert',
        'DELETE':            'Datensatz gelöscht',
        'ROLE_CHANGE':       'Rolle geändert',
      };

      const isUserUpdate =
        entry.resource_type === AuditResourceType.USER &&
        entry.action === AuditAction.UPDATE;
      const isSensitive = sensitiveActions[entry.action] || isUserUpdate;

      if (!isSensitive) return;

      // Get all active admin users
      const { rows: admins } = await pool.query(
        "SELECT id FROM users WHERE is_active = TRUE AND 'dashboard_admin' = ANY(authentik_groups)"
      );

      if (admins.length === 0) return;

      const titel = sensitiveActions[entry.action] || 'Benutzer-Änderung';

      let nachricht: string;
      if (entry.action === 'PERMISSION_DENIED') {
        const meta = entry.metadata as Record<string, unknown> | undefined;
        const perm = meta?.required_permission ?? '?';
        const path = meta?.attempted_path ?? '';
        const method = meta?.attempted_method ?? '';
        const userLabel = entry.user_email ?? 'Unbekannt';
        nachricht = `${userLabel}: ${method} ${path} — benötigt "${perm}"`;
      } else {
        nachricht = `${entry.action} auf ${entry.resource_type}${entry.resource_id ? ' ' + entry.resource_id : ''} durch ${entry.user_email ?? 'System'}`;
      }

      // Include user name/email in quell_id so each user+action combo is unique
      const quellId = entry.action === 'PERMISSION_DENIED'
        ? `${entry.action}_${(entry.metadata as any)?.required_permission ?? ''}_${entry.user_id ?? Date.now()}`
        : `${entry.action}_${entry.resource_type}_${entry.resource_id ?? Date.now()}`;

      for (const admin of admins) {
        // Don't notify the admin about their own actions
        if (admin.id === entry.user_id) continue;

        await notificationService.createNotification({
          user_id:   admin.id,
          typ:       'audit_alert',
          titel,
          nachricht,
          schwere:   entry.action === 'PERMISSION_DENIED' ? 'warnung' : 'info',
          quell_typ: 'audit_alert',
          quell_id:  quellId,
        });
      }
    } catch (error) {
      logger.error('alertAdminsIfSensitive failed', {
        error:  error instanceof Error ? error.message : String(error),
        action: entry.action,
      });
    }
  }

  // -------------------------------------------------------------------------
  // Query — admin UI
  // -------------------------------------------------------------------------

  /**
   * getAuditLog — paginated, filtered query for the admin dashboard.
   *
   * Never called from hot paths; can be awaited normally.
   */
  async getAuditLog(filters: AuditFilters): Promise<AuditLogPage> {
    const page     = Math.max(1, filters.page ?? 1);
    const pageSize = Math.min(200, Math.max(1, filters.pageSize ?? 25));
    const offset   = (page - 1) * pageSize;

    const conditions: string[] = [];
    const values: unknown[]    = [];
    let   paramIndex           = 1;

    if (filters.userId) {
      conditions.push(`user_id = $${paramIndex++}`);
      values.push(filters.userId);
    }

    if (filters.action) {
      const actions = Array.isArray(filters.action)
        ? filters.action
        : [filters.action];
      conditions.push(`action = ANY($${paramIndex++}::audit_action[])`);
      values.push(actions);
    }

    if (filters.resourceType) {
      const types = Array.isArray(filters.resourceType)
        ? filters.resourceType
        : [filters.resourceType];
      conditions.push(`resource_type = ANY($${paramIndex++}::audit_resource_type[])`);
      values.push(types);
    }

    if (filters.resourceId) {
      conditions.push(`resource_id = $${paramIndex++}`);
      values.push(filters.resourceId);
    }

    if (filters.dateFrom) {
      conditions.push(`created_at >= $${paramIndex++}`);
      values.push(filters.dateFrom);
    }

    if (filters.dateTo) {
      conditions.push(`created_at <= $${paramIndex++}`);
      values.push(filters.dateTo);
    }

    const whereClause = conditions.length > 0
      ? `WHERE ${conditions.join(' AND ')}`
      : '';

    const countQuery = `
      SELECT COUNT(*) AS total
      FROM audit_log
      ${whereClause}
    `;

    const dataQuery = `
      SELECT
        id, user_id, user_email, action, resource_type, resource_id,
        old_value, new_value, ip_address, user_agent, metadata, created_at
      FROM audit_log
      ${whereClause}
      ORDER BY created_at DESC
      LIMIT $${paramIndex++} OFFSET $${paramIndex++}
    `;

    const [countResult, dataResult] = await Promise.all([
      pool.query(countQuery, values),
      pool.query(dataQuery,  [...values, pageSize, offset]),
    ]);

    const total   = parseInt(countResult.rows[0].total, 10);
    const entries = dataResult.rows as AuditLogEntry[];

    return {
      entries,
      total,
      page,
      pages: Math.ceil(total / pageSize),
    };
  }

  // -------------------------------------------------------------------------
  // GDPR IP anonymisation — run as a scheduled job
  // -------------------------------------------------------------------------

  /**
   * anonymizeOldIpAddresses
   *
   * Replaces the ip_address of every audit_log row older than 90 days with
   * the literal string '[anonymized]'.  This satisfies the GDPR storage
   * limitation principle (Art. 5(1)(e)) for IP addresses as personal data.
   *
   * Uses a single UPDATE with a WHERE clause covered by idx_audit_log_ip_retention,
   * so performance is proportional to the number of rows being anonymised, not
   * the total table size.
   *
   * Note: The database RULE audit_log_no_update blocks UPDATE statements issued
   * *by application queries*, but that rule is created with DO INSTEAD NOTHING,
   * which means the application-level anonymisation query also cannot run unless
   * the rule is dropped or replaced.
   *
   * Resolution: the migration creates the rule.  For the anonymisation job to
   * work, the database role used by this application must either:
   *   (a) own the table (then rules do not apply to the owner), OR
   *   (b) use a separate privileged role for the anonymisation query only.
   *
   * Recommended approach: use option (a) by ensuring DB_USER is the table
   * owner, which is the default when the same user runs the migrations.
   * The immutability rule then protects against accidental application-level
   * UPDATE/DELETE while the owner can still perform sanctioned data operations.
   */
  async anonymizeOldIpAddresses(): Promise<void> {
    try {
      const result = await pool.query(`
        UPDATE audit_log
        SET ip_address = '[anonymized]'
        WHERE created_at < NOW() - INTERVAL '90 days'
          AND ip_address IS NOT NULL
          AND ip_address != '[anonymized]'
      `);

      const count = result.rowCount ?? 0;
      if (count > 0) {
        logger.info('GDPR IP anonymisation complete', { rows_anonymized: count });
      } else {
        logger.debug('GDPR IP anonymisation: no rows required anonymisation');
      }
    } catch (error) {
      logger.error('GDPR IP anonymisation job failed', {
        error: error instanceof Error ? error.message : String(error),
      });
      // Do not re-throw: the job scheduler should not crash the process.
    }
  }

  // -------------------------------------------------------------------------
  // CSV export helper (used by the admin API route)
  // -------------------------------------------------------------------------

  /**
   * Converts a page of audit entries to CSV format.
   * Passwords and secrets are already stripped; old_value/new_value are
   * serialised as compact JSON strings within the CSV cell.
   */
  entriesToCsv(entries: AuditLogEntry[]): string {
    const header = [
      'id', 'created_at', 'user_id', 'user_email',
      'action', 'resource_type', 'resource_id',
      'ip_address', 'user_agent', 'old_value', 'new_value', 'metadata',
    ].join(',');

    const escape = (v: unknown): string => {
      if (v === null || v === undefined) return '';
      const str = typeof v === 'object' ? JSON.stringify(v) : String(v);
      let safe = str.replace(/"/g, '""');
      // Prevent formula injection in spreadsheets
      if (/^[=+@\-]/.test(safe)) safe = "'" + safe;
      return `"${safe}"`;
    };

    const rows = entries.map((e) =>
      [
        escape(e.id),
        escape(e.created_at instanceof Date ? e.created_at.toISOString() : String(e.created_at)),
        escape(e.user_id    ?? ''),
        escape(e.user_email ?? ''),
        escape(e.action),
        escape(e.resource_type),
        escape(e.resource_id ?? ''),
        escape(e.ip_address  ?? ''),
        escape(e.user_agent),
        escape(e.old_value),
        escape(e.new_value),
        escape(e.metadata),
      ].join(',')
    );

    return [header, ...rows].join('\r\n');
  }
}

export default new AuditService();
