import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';

export type AiTraceSource = 'receipt' | 'flyer';

export type AiTraceStatus = 'success' | 'warning' | 'error';

const AI_TRACE_MASK_FIELDS = ['personnummer', 'telefon', 'email', 'address', 'namn'];
const SWEDISH_PERSONAL_ID_REGEX = /\b(\d{2})?(\d{6})[-+ ]?(\d{4})\b/g;
const EMAIL_REGEX = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
const PHONE_REGEX = /\b(?:\+46|0)\s?\d(?:[\d\s-]{6,}\d)\b/g;

export type AiTraceListItem = {
  id: string;
  source: AiTraceSource;
  status: AiTraceStatus;
  createdAt: string;
  userId: number;
  userLabel: string;
  sessionId: number | null;
  fileName: string | null;
  model: string | null;
  durationMs: number | null;
  warningsCount: number;
  hasPrompt: boolean;
  hasOutput: boolean;
  error: string | null;
};

export type AiTraceListResponse = {
  items: AiTraceListItem[];
  nextCursor: string | null;
};

export type AiTraceDetail = {
  id: string;
  source: AiTraceSource;
  status: AiTraceStatus;
  createdAt: string;
  userId: number;
  userLabel: string;
  sessionId: number | null;
  fileName: string | null;
  model: string | null;
  durationMs: number | null;
  retryCount: number | null;
  chunkCount: number | null;
  warnings: string[];
  error: string | null;
  prompt: string | null;
  rawOutput: string | null;
  normalizedOutput: Record<string, unknown> | null;
  summary: Record<string, unknown>;
};

type FlyerTraceSupplement = {
  prompt: string | null;
  rawOutput: string | null;
  retryCount: number | null;
  chunkCount: number | null;
};

@Injectable()
export class AiTraceService {
  constructor(private readonly prisma: PrismaService) {}

  async listTraces(params: {
    source: AiTraceSource;
    limit: number;
    cursor?: string;
    period?: '24h' | '7d' | '30d';
    onlyErrors?: boolean;
  }): Promise<AiTraceListResponse> {
    if (params.source === 'receipt') {
      return this.listReceiptTraces(params);
    }

    const take = Math.max(1, Math.min(params.limit || 20, 100));
    const cursorId = this.parseCursor(params.cursor);
    const periodStart = this.periodStart(params.period);

    const sessions = await this.prisma.flyerSession.findMany({
      where: {
        ...(periodStart ? { createdAt: { gte: periodStart } } : {}),
        ...(cursorId ? { id: { lt: cursorId } } : {}),
        ...(params.onlyErrors ? { items: { none: {} } } : {}),
      },
      orderBy: { id: 'desc' },
      take: take + 1,
      select: {
        id: true,
        userId: true,
        createdAt: true,
        sourceFileName: true,
        user: { select: { username: true, email: true } },
        items: {
          select: {
            id: true,
            parseReasons: true,
            matchReasons: true,
          },
        },
      },
    });

    const hasMore = sessions.length > take;
    const page = hasMore ? sessions.slice(0, take) : sessions;

    const items: AiTraceListItem[] = page.map((session) => {
      const warningsCount = session.items.reduce((sum, item) => {
        const parseWarnings = Array.isArray(item.parseReasons) ? item.parseReasons.length : 0;
        const matchWarnings = Array.isArray(item.matchReasons) ? item.matchReasons.length : 0;
        return sum + parseWarnings + matchWarnings;
      }, 0);
      const status = this.statusFromSession(session.items.length, warningsCount);
      return {
        id: this.flyerTraceId(session.id),
        source: 'flyer',
        status,
        createdAt: session.createdAt.toISOString(),
        userId: session.userId,
        userLabel: this.userLabel(session.user?.username, session.user?.email, session.userId),
        sessionId: session.id,
        fileName: session.sourceFileName,
        model: 'ministral-8b-2512',
        durationMs: null,
        warningsCount,
        hasPrompt: false,
        hasOutput: session.items.length > 0,
        error: status === 'error' ? 'Inga produkter kunde extraheras från flyern.' : null,
      };
    });

    const supplements = await this.getFlyerTraceSupplements(page.map((session) => session.id));

    const withSupplements = items.map((item) => {
      const sessionId = item.sessionId ?? 0;
      const supplement = supplements.get(sessionId);
      const hasPrompt = item.hasPrompt || !!supplement?.prompt;
      const hasOutput = item.hasOutput || !!supplement?.rawOutput;
      const error = item.status === 'warning' && item.warningsCount > 0
        ? `Det finns ${item.warningsCount} varningar i detaljvyn.`
        : item.error;
      return {
        ...item,
        hasPrompt,
        hasOutput,
        error,
      };
    });

    return {
      items: withSupplements,
      nextCursor: hasMore ? String(page[page.length - 1]?.id ?? '') : null,
    };
  }

  async getTraceById(id: string): Promise<AiTraceDetail> {
    const parsed = this.parseTraceId(id);
    if (parsed.source === 'receipt') {
      return this.getReceiptTraceById(parsed.numericId);
    }

    const session = await this.prisma.flyerSession.findUnique({
      where: { id: parsed.numericId },
      select: {
        id: true,
        userId: true,
        createdAt: true,
        sourceFileName: true,
        sourceMimeType: true,
        sourceFileSize: true,
        user: { select: { username: true, email: true } },
        items: {
          orderBy: { id: 'asc' },
          select: {
            id: true,
            rawName: true,
            normalizedName: true,
            brand: true,
            categoryHint: true,
            categoryId: true,
            price: true,
            priceUnit: true,
            comparisonPrice: true,
            comparisonUnit: true,
            weight: true,
            bundleWeight: true,
            isBundle: true,
            bundleItems: true,
            offerText: true,
            parseConfidence: true,
            parseReasons: true,
            matchedProductId: true,
            matchedProductName: true,
            matchedVia: true,
            matchConfidence: true,
            matchReasons: true,
          },
        },
      },
    });

    if (!session) {
      throw new NotFoundException('AI-trace hittades inte.');
    }

    const warnings = this.collectWarnings(session.items);
    const status = this.statusFromSession(session.items.length, warnings.length);
    const supplement = await this.getFlyerTraceSupplementBySessionId(session.id);

    const normalizedOutput = {
      sessionId: session.id,
      source: 'flyer',
      sourceFileName: session.sourceFileName,
      sourceMimeType: session.sourceMimeType,
      sourceFileSize: session.sourceFileSize,
      itemCount: session.items.length,
      items: session.items.map((item) => ({
        id: item.id,
        rawName: item.rawName,
        normalizedName: item.normalizedName,
        brand: item.brand,
        categoryHint: item.categoryHint,
        categoryId: item.categoryId,
        price: item.price != null ? Number(item.price) : null,
        priceUnit: item.priceUnit,
        comparisonPrice: item.comparisonPrice != null ? Number(item.comparisonPrice) : null,
        comparisonUnit: item.comparisonUnit,
        weight: item.weight,
        bundleWeight: item.bundleWeight,
        isBundle: item.isBundle,
        bundleItems: Array.isArray(item.bundleItems) ? item.bundleItems : [],
        offerText: item.offerText,
        parseConfidence: item.parseConfidence,
        parseReasons: Array.isArray(item.parseReasons) ? item.parseReasons : [],
        matchedProductId: item.matchedProductId,
        matchedProductName: item.matchedProductName,
        matchedVia: item.matchedVia,
        matchConfidence: item.matchConfidence,
        matchReasons: Array.isArray(item.matchReasons) ? item.matchReasons : [],
      })),
      warnings,
    } as Record<string, unknown>;

    return {
      id: this.flyerTraceId(session.id),
      source: 'flyer',
      status,
      createdAt: session.createdAt.toISOString(),
      userId: session.userId,
      userLabel: this.userLabel(session.user?.username, session.user?.email, session.userId),
      sessionId: session.id,
      fileName: session.sourceFileName,
      model: 'ministral-8b-2512',
      durationMs: null,
      retryCount: supplement.retryCount,
      chunkCount: supplement.chunkCount,
      warnings,
      error: session.items.length === 0 ? 'Inga produkter kunde extraheras från flyern.' : null,
      prompt: supplement.prompt,
      rawOutput:
        this.maskRawOutput(supplement.rawOutput) ?? JSON.stringify(this.maskSensitiveData(normalizedOutput)),
      normalizedOutput: this.maskSensitiveData(normalizedOutput),
      summary: {
        source: 'flyer',
        sessionId: session.id,
        itemCount: session.items.length,
        warningsCount: warnings.length,
        promptAvailable: !!supplement.prompt,
        outputAvailable: true,
        retentionHintDays: 30,
        maskedFields: AI_TRACE_MASK_FIELDS,
      },
    };
  }

  private async getFlyerTraceSupplements(sessionIds: number[]): Promise<Map<number, FlyerTraceSupplement>> {
    if (sessionIds.length === 0) return new Map<number, FlyerTraceSupplement>();

    const rows = await this.prisma.aiTrace.findMany({
      where: {
        source: 'flyer',
        sessionId: { in: sessionIds },
      },
      orderBy: [{ sessionId: 'desc' }, { createdAt: 'desc' }],
      select: {
        sessionId: true,
        prompt: true,
        rawOutput: true,
        normalizedOutput: true,
      },
    });

    const out = new Map<number, FlyerTraceSupplement>();
    for (const row of rows) {
      if (row.sessionId == null || out.has(row.sessionId)) continue;
      out.set(row.sessionId, {
        prompt: row.prompt ? this.maskSensitiveText(row.prompt) : null,
        rawOutput: row.rawOutput,
        retryCount: this.extractTraceNumber(row.normalizedOutput, 'retryCount'),
        chunkCount: this.extractTraceNumber(row.normalizedOutput, 'chunkCount'),
      });
    }
    return out;
  }

  private async getFlyerTraceSupplementBySessionId(sessionId: number): Promise<FlyerTraceSupplement> {
    const rows = await this.getFlyerTraceSupplements([sessionId]);
    return rows.get(sessionId) ?? {
      prompt: null,
      rawOutput: null,
      retryCount: null,
      chunkCount: null,
    };
  }

  private extractTraceNumber(value: unknown, key: string): number | null {
    if (!value || typeof value !== 'object') return null;
    const entry = (value as Record<string, unknown>)[key];
    if (typeof entry === 'number' && Number.isFinite(entry)) return entry;
    if (typeof entry === 'string') {
      const parsed = Number.parseInt(entry, 10);
      return Number.isFinite(parsed) ? parsed : null;
    }
    return null;
  }

  private statusFromSession(itemCount: number, warningsCount: number): AiTraceStatus {
    if (itemCount <= 0) return 'error';
    if (warningsCount > 0) return 'warning';
    return 'success';
  }

  private maskSensitiveData(data: Record<string, unknown>): Record<string, unknown> {
    const clone = JSON.parse(JSON.stringify(data)) as Record<string, unknown>;
    return this.maskDeep(clone) as Record<string, unknown>;
  }

  private maskDeep(value: unknown): unknown {
    if (typeof value === 'string') {
      return this.maskSensitiveText(value);
    }
    if (Array.isArray(value)) {
      return value.map((entry) => this.maskDeep(entry));
    }
    if (value && typeof value === 'object') {
      const out: Record<string, unknown> = {};
      for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
        const lowerKey = key.toLowerCase();
        if (AI_TRACE_MASK_FIELDS.some((field) => lowerKey.includes(field))) {
          out[key] = '[MASKED]';
          continue;
        }
        out[key] = this.maskDeep(nested);
      }
      return out;
    }
    return value;
  }

  private maskSensitiveText(value: string): string {
    return value
      .replace(EMAIL_REGEX, '[MASKED]')
      .replace(SWEDISH_PERSONAL_ID_REGEX, '[MASKED]')
      .replace(PHONE_REGEX, '[MASKED]');
  }

  private maskRawOutput(rawOutput: string | null | undefined): string | null {
    if (typeof rawOutput !== 'string' || rawOutput.trim().length === 0) {
      return null;
    }

    try {
      const parsed = JSON.parse(rawOutput);
      if (parsed && typeof parsed === 'object') {
        const masked = this.maskDeep(parsed);
        return JSON.stringify(masked);
      }
      if (typeof parsed === 'string') {
        return this.maskSensitiveText(parsed);
      }
      return this.maskSensitiveText(String(parsed));
    } catch {
      return this.maskSensitiveText(rawOutput);
    }
  }

  private parseCursor(cursor?: string): number | null {
    if (!cursor) return null;
    const value = Number.parseInt(cursor, 10);
    return Number.isFinite(value) && value > 0 ? value : null;
  }

  private periodStart(period?: '24h' | '7d' | '30d'): Date | null {
    if (!period) return null;
    const now = Date.now();
    const map: Record<string, number> = {
      '24h': 24 * 60 * 60 * 1000,
      '7d': 7 * 24 * 60 * 60 * 1000,
      '30d': 30 * 24 * 60 * 60 * 1000,
    };
    const duration = map[period];
    if (!duration) return null;
    return new Date(now - duration);
  }

  private parseTraceId(id: string): { source: AiTraceSource; numericId: number } {
    const trimmed = id.trim();
    if (trimmed.startsWith('flyer-')) {
      const value = Number.parseInt(trimmed.replace('flyer-', ''), 10);
      if (Number.isFinite(value) && value > 0) {
        return { source: 'flyer', numericId: value };
      }
    }
    if (trimmed.startsWith('receipt-')) {
      const value = Number.parseInt(trimmed.replace('receipt-', ''), 10);
      if (Number.isFinite(value) && value > 0) {
        return { source: 'receipt', numericId: value };
      }
    }
    throw new NotFoundException('AI-trace hittades inte.');
  }

  private flyerTraceId(sessionId: number): string {
    return `flyer-${sessionId}`;
  }

  private userLabel(username: string | null | undefined, email: string | null | undefined, userId: number): string {
    if (username && username.trim().length > 0) return username.trim();
    if (email && email.trim().length > 0) return email.trim();
    return `user:${userId}`;
  }

  private collectWarnings(items: Array<{ parseReasons: unknown; matchReasons: unknown }>): string[] {
    const warnings = new Set<string>();
    for (const item of items) {
      if (Array.isArray(item.parseReasons)) {
        for (const reason of item.parseReasons) {
          const text = String(reason ?? '').trim();
          if (text.length > 0) warnings.add(`parse:${text}`);
        }
      }
      if (Array.isArray(item.matchReasons)) {
        for (const reason of item.matchReasons) {
          const text = String(reason ?? '').trim();
          if (text.length > 0) warnings.add(`match:${text}`);
        }
      }
    }
    return Array.from(warnings);
  }

  private async listReceiptTraces(params: {
    source: AiTraceSource;
    limit: number;
    cursor?: string;
    period?: '24h' | '7d' | '30d';
    onlyErrors?: boolean;
  }): Promise<AiTraceListResponse> {
    const take = Math.max(1, Math.min(params.limit || 20, 100));
    const cursorId = this.parseCursor(params.cursor);
    const periodStart = this.periodStart(params.period);

    const rows = await this.prisma.aiTrace.findMany({
      where: {
        source: 'receipt',
        ...(periodStart ? { createdAt: { gte: periodStart } } : {}),
        ...(cursorId ? { id: { lt: cursorId } } : {}),
        ...(params.onlyErrors ? { status: 'error' } : {}),
      },
      orderBy: { id: 'desc' },
      take: take + 1,
      select: {
        id: true,
        source: true,
        status: true,
        createdAt: true,
        userId: true,
        sessionId: true,
        model: true,
        durationMs: true,
        error: true,
        prompt: true,
        rawOutput: true,
        user: { select: { username: true, email: true } },
      },
    });

    const hasMore = rows.length > take;
    const page = hasMore ? rows.slice(0, take) : rows;

    return {
      items: page.map((row) => ({
        id: `receipt-${row.id}`,
        source: 'receipt',
        status: row.status === 'error' ? 'error' : row.status === 'warning' ? 'warning' : 'success',
        createdAt: row.createdAt.toISOString(),
        userId: row.userId ?? 0,
        userLabel: this.userLabel(row.user?.username, row.user?.email, row.userId ?? 0),
        sessionId: row.sessionId,
        fileName: null,
        model: row.model,
        durationMs: row.durationMs,
        warningsCount: 0,
        hasPrompt: !!row.prompt,
        hasOutput: !!row.rawOutput,
        error: row.error,
      })),
      nextCursor: hasMore ? String(page[page.length - 1]?.id ?? '') : null,
    };
  }

  private async getReceiptTraceById(traceId: number): Promise<AiTraceDetail> {
    const row = await this.prisma.aiTrace.findFirst({
      where: { id: traceId, source: 'receipt' },
      select: {
        id: true,
        source: true,
        status: true,
        createdAt: true,
        userId: true,
        sessionId: true,
        model: true,
        durationMs: true,
        error: true,
        prompt: true,
        rawOutput: true,
        normalizedOutput: true,
        user: { select: { username: true, email: true } },
      },
    });

    if (!row) {
      throw new NotFoundException('AI-trace hittades inte.');
    }

    const normalizedOutput = row.normalizedOutput && typeof row.normalizedOutput === 'object'
      ? this.maskSensitiveData(row.normalizedOutput as Record<string, unknown>)
      : null;

    return {
      id: `receipt-${row.id}`,
      source: 'receipt',
      status: row.status === 'error' ? 'error' : row.status === 'warning' ? 'warning' : 'success',
      createdAt: row.createdAt.toISOString(),
      userId: row.userId ?? 0,
      userLabel: this.userLabel(row.user?.username, row.user?.email, row.userId ?? 0),
      sessionId: row.sessionId,
      fileName: null,
      model: row.model,
      durationMs: row.durationMs,
      retryCount: null,
      chunkCount: null,
      warnings: [],
      error: row.error,
      prompt: row.prompt ? this.maskSensitiveText(row.prompt) : null,
      rawOutput: this.maskRawOutput(row.rawOutput),
      normalizedOutput,
      summary: {
        source: 'receipt',
        traceId: row.id,
        promptAvailable: !!row.prompt,
        outputAvailable: !!row.rawOutput || normalizedOutput != null,
        retentionHintDays: 30,
        maskedFields: AI_TRACE_MASK_FIELDS,
      },
    };
  }
}
