/**
 * Notify Step — send notification to a configured channel.
 * Config: { channel_id: 1, message: "...", card_template?: {...} }
 * Channel config is loaded from notification_channels table.
 *
 * Optional route_to_user block (Phase 9 ROUTE-01..06):
 * When present, the step attempts delivery via the resolved Pulse user's
 * personal channel before falling back to the step's channel_id.
 * When absent, behavior is byte-identical to the pre-Phase-9 implementation.
 */

import { registerStepExecutor } from '../pipeline-engine';
import { postgresClient } from '../postgres-client';
import {
  PipelineStep,
  PipelineContext,
  StepExecutorResult,
  NotificationChannel,
  RouteToUser,
  UserRouteFallback,
} from '../../types/pipeline';
import { resolveRecipient } from './notify-resolvers';

// ============================================================================
// Main executor
// ============================================================================

async function executeNotify(
  step: PipelineStep,
  context: PipelineContext,
  _executionId: number,
): Promise<StepExecutorResult> {
  const channelId = Number(step.config.channel_id);
  if (!channelId || isNaN(channelId)) {
    return { success: false, error: 'Missing or invalid channel_id' };
  }

  const route = (step.config.route_to_user as RouteToUser | undefined) ?? null;
  const message = step.config.message || '';

  // BACKWARD COMPATIBLE PATH: no route_to_user → original behavior verbatim.
  if (!route) {
    return await dispatchToGlobalChannel(channelId, step.config, message);
  }

  // USER-ROUTE PATH (ROUTE-01..06)
  return await dispatchUserRoute({ step, context, message, channelId, route });
}

// ============================================================================
// Global channel dispatch (original path — extracted for reuse as fallback)
// ============================================================================

async function dispatchToGlobalChannel(
  channelId: number,
  config: Record<string, any>,
  message: string,
): Promise<StepExecutorResult> {
  const result = await postgresClient.query<NotificationChannel>(
    `SELECT * FROM notification_channels WHERE id = $1 AND is_active = true`,
    [channelId],
  );

  if (result.rows.length === 0) {
    return { success: false, error: `Notification channel #${channelId} not found or inactive` };
  }

  const channel = result.rows[0];

  switch (channel.channel_type) {
    case 'teams':
      return await sendTeams(channel, config, message);
    case 'telegram':
      return await sendTelegram(channel, message);
    case 'ntfy':
      return await sendNtfy(channel, config, message);
    case 'webhook':
      return await sendWebhook(channel, config, message);
    default:
      return { success: false, error: `Unknown channel type: ${channel.channel_type}` };
  }
}

// ============================================================================
// Per-user route dispatch (ROUTE-03..06, D-08..D-12)
// ============================================================================

async function dispatchUserRoute(args: {
  step: PipelineStep;
  context: PipelineContext;
  message: string;
  channelId: number;
  route: RouteToUser;
}): Promise<StepExecutorResult> {
  const { step, context, message, channelId, route } = args;

  // Step 5a — Read field value from context (single-level lookup, v1 limitation).
  // v1: single-level only. Dotted paths like 'company.id' are NOT walked.
  // Admins must surface nested values at top level via an upstream transform.
  const sourceObj = context[route.source];
  const fieldValue = sourceObj?.[route.field];
  if (fieldValue === undefined || fieldValue === null || fieldValue === '') {
    return await fallbackToGlobal({
      channelId,
      config: step.config,
      message,
      fallback: { reason: 'no_field_value', channel_type: route.channel_type ?? 'auto' },
    });
  }

  // Step 5b — Resolve recipient via the named resolver.
  const { recipient, resolverFound } = await resolveRecipient(route.resolve, fieldValue);
  if (!resolverFound) {
    return await fallbackToGlobal({
      channelId,
      config: step.config,
      message,
      fallback: {
        reason: 'resolver_unknown',
        channel_type: route.channel_type ?? 'auto',
        error: `Unknown resolver: ${route.resolve}`,
      },
    });
  }
  if (!recipient) {
    return await fallbackToGlobal({
      channelId,
      config: step.config,
      message,
      fallback: { reason: 'user_not_found', channel_type: route.channel_type ?? 'auto' },
    });
  }

  // Step 5c — Resolve recipient to a Pulse user.id.
  let userId: string;
  if ('user_id' in recipient) {
    userId = recipient.user_id;
  } else {
    // Has email — look up Pulse user by email (case-insensitive; Autotask emails vary).
    const userRes = await postgresClient.query<{ id: string }>(
      `SELECT id FROM "user" WHERE LOWER(email) = LOWER($1)`,
      [recipient.email],
    );
    if (userRes.rows.length === 0) {
      return await fallbackToGlobal({
        channelId,
        config: step.config,
        message,
        fallback: { reason: 'user_not_found', channel_type: route.channel_type ?? 'auto' },
      });
    }
    userId = userRes.rows[0].id;
  }

  // Step 5d — Determine channel-type attempt order (ROUTE-06 / D-10).
  // When channel_type is omitted: ntfy first (instant push), then teams.
  const attemptOrder: Array<'ntfy' | 'teams'> =
    route.channel_type ? [route.channel_type] : ['ntfy', 'teams'];

  // Step 5e — For each attempted type: check mute, look up personal channel, send.
  for (const channelType of attemptOrder) {
    // ROUTE-05 mute check — query user_event_subscriptions.
    // Default-enabled (D-15): row absence = enabled = true.
    const subRes = await postgresClient.query<{ enabled: boolean }>(
      `SELECT enabled FROM user_event_subscriptions
       WHERE user_id = $1 AND event_key = $2 AND channel_type = $3`,
      [userId, route.event_key, channelType],
    );
    const enabled = subRes.rows.length === 0 ? true : subRes.rows[0].enabled;
    if (!enabled) {
      // ROUTE-05 / D-12: skip silently — muting must actually mute.
      // DO NOT call fallbackToGlobal here. Return without any fallback.
      return {
        success: true,
        output: {
          notified: false,
          skipped_reason: 'user_muted',
          user_id: userId,
          event_key: route.event_key,
          channel_type: channelType,
        },
      };
    }

    // Look up the user's personal channel of this type.
    const chanRes = await postgresClient.query<NotificationChannel>(
      `SELECT * FROM notification_channels
       WHERE owner_user_id = $1 AND channel_type = $2 AND is_active = true
       LIMIT 1`,
      [userId, channelType],
    );
    if (chanRes.rows.length === 0) {
      // No personal channel of this type.
      // If more types remain in attemptOrder, continue to the next.
      // If this was the last type, fall back to global.
      if (channelType === attemptOrder[attemptOrder.length - 1]) {
        return await fallbackToGlobal({
          channelId,
          config: step.config,
          message,
          fallback: { reason: 'no_channel', user_id: userId, channel_type: channelType },
        });
      }
      continue;
    }

    // Dispatch to the personal channel using existing send helpers.
    const dispatchResult =
      channelType === 'teams'
        ? await sendTeams(chanRes.rows[0], step.config, message)
        : await sendNtfy(chanRes.rows[0], step.config, message);

    if (dispatchResult.success) {
      return {
        success: true,
        output: {
          ...(dispatchResult.output ?? {}),
          user_route: { user_id: userId, channel_type: channelType, event_key: route.event_key },
        },
      };
    }

    // Send failed — try next type if available, else fall back to global.
    if (channelType === attemptOrder[attemptOrder.length - 1]) {
      return await fallbackToGlobal({
        channelId,
        config: step.config,
        message,
        fallback: {
          reason: 'send_failed',
          user_id: userId,
          channel_type: channelType,
          error: dispatchResult.error,
        },
      });
    }
  }

  // Defensive: the loop always returns or continues, so this should never run.
  return await fallbackToGlobal({
    channelId,
    config: step.config,
    message,
    fallback: { reason: 'no_channel', user_id: userId!, channel_type: 'auto' },
  });
}

// ============================================================================
// Fallback to global channel (D-11)
// ============================================================================

async function fallbackToGlobal(args: {
  channelId: number;
  config: Record<string, any>;
  message: string;
  fallback: UserRouteFallback;
}): Promise<StepExecutorResult> {
  const result = await dispatchToGlobalChannel(args.channelId, args.config, args.message);
  // Annotate output with the fallback reason regardless of global success/failure.
  if (result.success) {
    return {
      success: true,
      output: {
        ...(result.output ?? {}),
        user_route_fallback: args.fallback,
      },
    };
  }
  // Global also failed — return error with fallback context attached.
  return {
    success: false,
    error: result.error,
    output: { user_route_fallback: args.fallback },
  };
}

// ============================================================================
// Test seam — exposes internals for vitest mocking without changing the API.
// Follows the _INTERNALS pattern from lib/services/analyzer/link-discovery.ts.
// ============================================================================

export const _INTERNALS = { dispatchToGlobalChannel, dispatchUserRoute, fallbackToGlobal };

// ============================================================================
// Send helpers (preserved verbatim from pre-Phase-9)
// ============================================================================

async function sendTeams(
  channel: NotificationChannel,
  config: Record<string, any>,
  message: string,
): Promise<StepExecutorResult> {
  const webhookUrl = channel.config.webhook_url;
  if (!webhookUrl) {
    return { success: false, error: 'Teams channel missing webhook_url' };
  }

  // If a card_template is provided, use it as Adaptive Card
  const body = config.card_template || {
    type: 'message',
    attachments: [{
      contentType: 'application/vnd.microsoft.card.adaptive',
      content: {
        type: 'AdaptiveCard',
        $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
        version: '1.4',
        body: [
          { type: 'TextBlock', text: config.title || 'Pulse Notification', weight: 'bolder', size: 'medium' },
          { type: 'TextBlock', text: message, wrap: true },
        ],
      },
    }],
  };

  const resp = await fetch(webhookUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });

  if (!resp.ok) {
    const errText = await resp.text();
    return { success: false, error: `Teams webhook failed (${resp.status}): ${errText.substring(0, 200)}` };
  }

  return { success: true, output: { notified: true, channel: 'teams' } };
}

async function sendTelegram(
  channel: NotificationChannel,
  message: string,
): Promise<StepExecutorResult> {
  const botToken = channel.config.bot_token;
  const chatId = channel.config.chat_id;

  if (!botToken || !chatId) {
    return { success: false, error: 'Telegram channel missing bot_token or chat_id' };
  }

  const resp = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      chat_id: chatId,
      text: message,
      parse_mode: channel.config.parse_mode || 'HTML',
    }),
  });

  if (!resp.ok) {
    const errText = await resp.text();
    return { success: false, error: `Telegram API failed (${resp.status}): ${errText.substring(0, 200)}` };
  }

  return { success: true, output: { notified: true, channel: 'telegram' } };
}

async function sendNtfy(
  channel: NotificationChannel,
  config: Record<string, any>,
  message: string,
): Promise<StepExecutorResult> {
  const topic = channel.config.topic;
  if (!topic) {
    return { success: false, error: 'ntfy channel missing topic' };
  }

  // Personal channels (owner_user_id set) are forced to the company ntfy
  // server with the company bearer token (UAT-FIX-01). Global / admin rows
  // (owner_user_id NULL) retain their existing config-driven behavior so
  // legacy ntfy.sh deployments and custom self-hosted instances keep working.
  const isPersonal = !!channel.owner_user_id;
  const serverUrl = isPersonal
    ? (process.env.NTFY_BASE_URL || 'https://ntfy.wulfconsulting.cloud')
    : (channel.config.server_url || 'https://ntfy.sh');

  const headers: Record<string, string> = {
    'Content-Type': 'text/plain',
  };

  if (config.title || channel.config.default_title) {
    headers['Title'] = config.title || channel.config.default_title;
  }
  if (config.priority || channel.config.default_priority) {
    headers['Priority'] = config.priority || channel.config.default_priority;
  }

  if (isPersonal) {
    const token = process.env.NTFY_PULSE_TOKEN;
    if (!token) {
      return { success: false, error: 'NTFY_PULSE_TOKEN not configured' };
    }
    headers['Authorization'] = `Bearer ${token}`;
  } else if (channel.config.auth_token) {
    headers['Authorization'] = `Bearer ${channel.config.auth_token}`;
  }

  const resp = await fetch(`${serverUrl}/${topic}`, {
    method: 'POST',
    headers,
    body: message,
  });

  if (!resp.ok) {
    const errText = await resp.text();
    return { success: false, error: `ntfy failed (${resp.status}): ${errText.substring(0, 200)}` };
  }

  return { success: true, output: { notified: true, channel: 'ntfy' } };
}

async function sendWebhook(
  channel: NotificationChannel,
  config: Record<string, any>,
  message: string,
): Promise<StepExecutorResult> {
  const url = channel.config.url;
  if (!url) {
    return { success: false, error: 'Webhook channel missing url' };
  }

  const method = channel.config.method || 'POST';
  const customHeaders = channel.config.headers || {};

  const body = config.body_template
    ? config.body_template
    : { message, timestamp: new Date().toISOString() };

  const resp = await fetch(url, {
    method,
    headers: {
      'Content-Type': 'application/json',
      ...customHeaders,
    },
    body: JSON.stringify(body),
  });

  if (!resp.ok) {
    const errText = await resp.text();
    return { success: false, error: `Webhook failed (${resp.status}): ${errText.substring(0, 200)}` };
  }

  return { success: true, output: { notified: true, channel: 'webhook' } };
}

registerStepExecutor('notify', executeNotify);
