import { Injectable } from '@nestjs/common';
import { ToolExecutionResult, ToolContext, ToolDefinition } from './tool.types';
import { LokiClient } from '../../outbound/loki/loki.client';

type RiskDetailArgs = {
  service?: 'mds' | 'epp' | 'edr' | string;
  start?: string;
  end?: string;
  timestamp_kst?: string;
  window_minutes?: number;
  range_minutes?: number;
  risk?: string;
  ip?: string;
  detect_name?: string;
  limit?: number;
  detail?: boolean;
};

type LokiStream = {
  stream?: Record<string, string | undefined>;
  values?: Array<[string, string]>;
};

const SERVICE_MAP: Record<string, string> = {
  mds: 'ahnlab_mds',
  epp: 'ahnlab_epp',
  edr: 'ahnlab_edr',
};

const DEFAULT_RANGE_MINUTES = 60;
const DEFAULT_WINDOW_MINUTES = 2;

@Injectable()
export class LokiRiskDetailLogsUsecase {
  constructor(private readonly lokiClient: LokiClient) {}

  definition(): ToolDefinition {
    return {
      name: 'loki.risk_detail_logs',
      description:
        'Fetch risk detail logs for MDS/EPP/EDR with flexible filters (KST timestamp, risk, src_ip, detect_name).',
      inputSchema: {
        type: 'object',
        properties: {
          service: { type: 'string', enum: ['mds', 'epp', 'edr'] },
          start: { type: 'string', description: 'RFC3339 or epoch (requires end)' },
          end: { type: 'string', description: 'RFC3339 or epoch (requires start)' },
          timestamp_kst: { type: 'string', description: 'KST timestamp (e.g. 2026-02-20T11:22:00)' },
          window_minutes: { type: 'integer', minimum: 1, maximum: 1440 },
          range_minutes: { type: 'integer', minimum: 1, maximum: 1440 },
          risk: { type: 'string', enum: ['Low', 'Medium', 'High'] },
          ip: { type: 'string', description: 'src_ip filter' },
          detect_name: { type: 'string' },
          limit: { type: 'integer', minimum: 1, maximum: 5000 },
          detail: { type: 'boolean', description: 'Return raw Loki payload when true' },
        },
        additionalProperties: false,
      },
      handler: async (args, context) => this.execute(args, context),
    };
  }

  async execute(args: unknown, _context: ToolContext): Promise<ToolExecutionResult> {
    const typedArgs = (args ?? {}) as RiskDetailArgs;

    try {
      const { start, end } = this.resolveTimeRange(typedArgs);
      const query = this.buildQuery(typedArgs);

      const payload = await this.lokiClient.queryRange({
        query,
        start,
        end,
        step: '30s',
        limit: typedArgs.limit,
      });

      if (typedArgs.detail) {
        return {
          output: [
            {
              kind: 'text',
              text: JSON.stringify(payload, null, 2),
            },
          ],
          isError: false,
        };
      }

      const summarized = this.summarizePayload(payload);

      return {
        output: [
          {
            kind: 'text',
            text: JSON.stringify(summarized, null, 2),
          },
        ],
        isError: false,
      };
    } catch (error) {
      const details = error instanceof Error ? error.message : String(error);
      return {
        output: [{ kind: 'text', text: `loki.risk_detail_logs failed: ${details}` }],
        isError: true,
      };
    }
  }

  private buildQuery(args: RiskDetailArgs): string {
    const serviceKey = SERVICE_MAP[(args.service ?? 'mds').toLowerCase()] ?? SERVICE_MAP.mds;
    const riskSelector = args.risk
      ? `risk="${this.escapeLabelValue(args.risk)}"`
      : 'risk=~"Low|Medium|High"';

    let query = `{service="${serviceKey}", ${riskSelector}} | json`;

    if (args.ip) {
      query += ` | src_ip="${this.escapeLabelValue(args.ip)}"`;
    }
    if (args.detect_name) {
      query += ` | detect_name="${this.escapeLabelValue(args.detect_name)}"`;
    }

    return query;
  }

  private resolveTimeRange(args: RiskDetailArgs): { start: string; end: string } {
    if ((args.start && !args.end) || (!args.start && args.end)) {
      throw new Error('start and end must be provided together');
    }

    if (args.start && args.end) {
      return { start: args.start, end: args.end };
    }

    if (args.timestamp_kst) {
      const windowMinutes = args.window_minutes ?? DEFAULT_WINDOW_MINUTES;
      const center = this.parseKstTimestamp(args.timestamp_kst);
      if (!center) {
        throw new Error('timestamp_kst must be in YYYY-MM-DDTHH:mm[:ss] or YYYY-MM-DD HH:mm[:ss]');
      }
      const start = new Date(center.getTime() - windowMinutes * 60 * 1000).toISOString();
      const end = new Date(center.getTime() + windowMinutes * 60 * 1000).toISOString();
      return { start, end };
    }

    const rangeMinutes = args.range_minutes ?? DEFAULT_RANGE_MINUTES;
    const end = new Date();
    const start = new Date(end.getTime() - rangeMinutes * 60 * 1000);
    return { start: start.toISOString(), end: end.toISOString() };
  }

  private parseKstTimestamp(value: string): Date | null {
    const trimmed = value.trim();
    if (/[zZ]|[+-]\d{2}:?\d{2}$/.test(trimmed)) {
      const parsed = new Date(trimmed);
      return Number.isNaN(parsed.getTime()) ? null : parsed;
    }

    const match = trimmed.match(
      /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/
    );
    if (!match) return null;

    const [, y, mo, d, h, mi, s] = match;
    const year = Number(y);
    const month = Number(mo) - 1;
    const day = Number(d);
    const hour = Number(h);
    const minute = Number(mi);
    const second = Number(s ?? '0');

    const utcMillis = Date.UTC(year, month, day, hour - 9, minute, second);
    return new Date(utcMillis);
  }

  private summarizePayload(payload: unknown): Array<Record<string, string | null>> {
    const data = payload as {
      data?: { result?: LokiStream[] };
    };
    const result = data?.data?.result ?? [];

    const summaries: Array<Record<string, string | null>> = [];

    for (const entry of result) {
      const stream = entry.stream ?? {};
      const values = entry.values ?? [];

      if (values.length === 0) {
        summaries.push(this.toSummary(stream, undefined));
        continue;
      }

      for (const value of values) {
        summaries.push(this.toSummary(stream, value[0]));
      }
    }

    return summaries;
  }

  private toSummary(stream: Record<string, string | undefined>, rawTimestamp?: string): Record<string, string | null> {
    const eventTime = stream.event_time ?? this.toIsoFromNano(rawTimestamp);
    return {
      event_time: eventTime,
      attacker: stream.attacker ?? null,
      src_ip: stream.src_ip ?? null,
      detect_name: stream.detect_name ?? null,
      risk: stream.risk ?? null,
      event_id: stream.event_id ?? null,
      file_size: stream.file_size ?? null,
      file_type: stream.file_type ?? null,
      md5_hash: stream.md5_hash ?? null,
      ingest_host: stream.ingest_host ?? null,
    };
  }

  private toIsoFromNano(value?: string): string | null {
    if (!value) return null;
    const num = Number(value);
    if (!Number.isFinite(num)) return null;
    const millis = Math.floor(num / 1e6);
    return new Date(millis).toISOString();
  }

  private escapeLabelValue(value: string): string {
    return value.replace(/\\\\/g, '\\\\\\\\').replace(/"/g, '\\\\"');
  }
}
