// ============================================================
// CustomFieldsService — CRUD fuer Definitionen + Werte
// ============================================================

import {
  Injectable,
  NotFoundException,
  ConflictException,
  BadRequestException,
  Logger,
} from '@nestjs/common';
import { Prisma } from '.prisma/crm-client';
import { CrmPrismaService } from '../prisma/crm-prisma.service';
import {
  CreateCustomFieldDto,
  CustomFieldEntityType,
  CustomFieldType,
} from './dto/create-custom-field.dto';
import { UpdateCustomFieldDto } from './dto/update-custom-field.dto';
import { SetCustomFieldValuesDto } from './dto/set-custom-field-values.dto';

// --------------------------------------------------------
// Response-Interface fuer Entity-Detail-Integration
// --------------------------------------------------------
export interface CustomFieldEntry {
  fieldDefId: string;
  name: string;
  label: string;
  fieldType: string;
  value: string | number | boolean | string[] | null;
}

@Injectable()
export class CustomFieldsService {
  private readonly logger = new Logger(CustomFieldsService.name);

  constructor(private readonly prisma: CrmPrismaService) {}

  // ============================================================
  // Slug-Generierung
  // ============================================================

  /**
   * Generiert einen DB-sicheren Slug aus dem Label.
   * "Kundennummer" -> "kundennummer"
   * "Messe / Event" -> "messe-event"
   * "USt-IdNr." -> "ust-idnr"
   * Umlaute: ae/oe/ue/ss
   */
  private slugify(label: string): string {
    return label
      .toLowerCase()
      .trim()
      .replace(/[äÄ]/g, 'ae')
      .replace(/[öÖ]/g, 'oe')
      .replace(/[üÜ]/g, 'ue')
      .replace(/ß/g, 'ss')
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-+|-+$/g, '')
      .replace(/-{2,}/g, '-');
  }

  // ============================================================
  // CRUD — Feld-Definitionen
  // ============================================================

  async createDefinition(tenantId: string, dto: CreateCustomFieldDto) {
    // Options-Validierung
    this.validateOptionsForFieldType(dto.fieldType, dto.options);

    const name = this.slugify(dto.label);
    if (!name) {
      throw new BadRequestException('Label ergibt keinen gueltigen internen Namen');
    }

    // Uniqueness pruefen
    const existing = await this.prisma.customFieldDef.findUnique({
      where: {
        tenantId_entityType_name: {
          tenantId,
          entityType: dto.entityType,
          name,
        },
      },
    });
    if (existing) {
      throw new ConflictException(
        `Ein Feld mit dem Namen "${name}" existiert bereits fuer ${dto.entityType}`,
      );
    }

    return this.prisma.customFieldDef.create({
      data: {
        tenantId,
        entityType: dto.entityType,
        name,
        label: dto.label,
        fieldType: dto.fieldType,
        options: dto.options ? (dto.options as unknown as Prisma.InputJsonValue) : undefined,
        isRequired: dto.isRequired ?? false,
        position: dto.position ?? 0,
      },
    });
  }

  async findAllDefinitions(tenantId: string, entityType?: CustomFieldEntityType) {
    const where: Prisma.CustomFieldDefWhereInput = { tenantId };

    if (entityType) {
      where.entityType = entityType;
    }

    return this.prisma.customFieldDef.findMany({
      where,
      orderBy: [{ position: 'asc' }, { label: 'asc' }],
      include: {
        _count: { select: { values: true } },
      },
    });
  }

  async findOneDefinition(tenantId: string, id: string) {
    const def = await this.prisma.customFieldDef.findFirst({
      where: { id, tenantId },
      include: {
        _count: { select: { values: true } },
      },
    });

    if (!def) {
      throw new NotFoundException('Feld-Definition nicht gefunden');
    }

    return def;
  }

  async updateDefinition(tenantId: string, id: string, dto: UpdateCustomFieldDto) {
    const existing = await this.findOneDefinition(tenantId, id);

    // Options nur fuer DROPDOWN/MULTI_SELECT erlauben
    if (dto.options !== undefined) {
      const isSelectType =
        existing.fieldType === CustomFieldType.DROPDOWN ||
        existing.fieldType === CustomFieldType.MULTI_SELECT;

      if (!isSelectType) {
        throw new BadRequestException(
          'options darf nur fuer DROPDOWN/MULTI_SELECT Felder gesetzt werden',
        );
      }

      if (dto.options.length === 0) {
        throw new BadRequestException(
          'options darf nicht leer sein fuer DROPDOWN/MULTI_SELECT',
        );
      }
    }

    // Label-Aenderung → Slug neu berechnen + Unique-Check
    let name: string | undefined;
    if (dto.label && dto.label !== existing.label) {
      name = this.slugify(dto.label);
      if (!name) {
        throw new BadRequestException('Label ergibt keinen gueltigen internen Namen');
      }

      if (name !== existing.name) {
        const duplicate = await this.prisma.customFieldDef.findFirst({
          where: {
            tenantId,
            entityType: existing.entityType,
            name,
            NOT: { id },
          },
        });
        if (duplicate) {
          throw new ConflictException(
            `Ein Feld mit dem Namen "${name}" existiert bereits fuer ${existing.entityType}`,
          );
        }
      }
    }

    const updateData: Prisma.CustomFieldDefUpdateInput = {};
    if (dto.label !== undefined) updateData.label = dto.label;
    if (name !== undefined) updateData.name = name;
    if (dto.options !== undefined) updateData.options = dto.options as unknown as Prisma.InputJsonValue;
    if (dto.isRequired !== undefined) updateData.isRequired = dto.isRequired;
    if (dto.position !== undefined) updateData.position = dto.position;

    return this.prisma.customFieldDef.update({
      where: { id },
      data: updateData,
      include: {
        _count: { select: { values: true } },
      },
    });
  }

  async removeDefinition(tenantId: string, id: string) {
    await this.findOneDefinition(tenantId, id);

    // Prisma Cascade loescht automatisch alle zugehoerigen Values
    return this.prisma.customFieldDef.delete({
      where: { id },
    });
  }

  // ============================================================
  // Werte — Bulk-Upsert + Lesen
  // ============================================================

  async setValues(tenantId: string, entityId: string, dto: SetCustomFieldValuesDto) {
    if (dto.values.length === 0) {
      return [];
    }

    // 1. Alle referenzierten Definitionen in einer Query laden
    const fieldDefIds = dto.values.map((v) => v.fieldDefId);
    const fieldDefs = await this.prisma.customFieldDef.findMany({
      where: {
        id: { in: fieldDefIds },
        tenantId,
      },
    });

    const defMap = new Map(fieldDefs.map((d) => [d.id, d]));

    // 2. Validierung: Alle fieldDefIds muessen existieren + Typ-Check
    for (const item of dto.values) {
      const def = defMap.get(item.fieldDefId);
      if (!def) {
        throw new NotFoundException(
          `Feld-Definition "${item.fieldDefId}" nicht gefunden`,
        );
      }
      this.validateValue(def.fieldType as CustomFieldType, def.label, def.options, item.value);
    }

    // 3. Upsert in Transaction
    return this.prisma.$transaction(async (tx) => {
      const results: Array<{
        fieldDefId: string;
        entityId: string;
        value: string | number | boolean | string[] | null;
      }> = [];

      for (const item of dto.values) {
        const def = defMap.get(item.fieldDefId)!;

        if (item.value === null) {
          // null = Wert loeschen
          await tx.customFieldValue.deleteMany({
            where: { fieldDefId: item.fieldDefId, entityId },
          });
          results.push({ fieldDefId: item.fieldDefId, entityId, value: null });
        } else {
          const columns = this.mapValueToColumns(def.fieldType as CustomFieldType, item.value);

          await tx.customFieldValue.upsert({
            where: {
              fieldDefId_entityId: {
                fieldDefId: item.fieldDefId,
                entityId,
              },
            },
            update: {
              ...columns,
            },
            create: {
              tenantId,
              fieldDefId: item.fieldDefId,
              entityId,
              ...columns,
            },
          });
          results.push({ fieldDefId: item.fieldDefId, entityId, value: item.value });
        }
      }

      return results;
    });
  }

  async getValues(tenantId: string, entityId: string) {
    const values = await this.prisma.customFieldValue.findMany({
      where: { tenantId, entityId },
      include: {
        fieldDef: {
          select: {
            id: true,
            name: true,
            label: true,
            fieldType: true,
            entityType: true,
            options: true,
            isRequired: true,
            position: true,
          },
        },
      },
      orderBy: { fieldDef: { position: 'asc' } },
    });

    return values.map((v) => ({
      fieldDefId: v.fieldDef.id,
      name: v.fieldDef.name,
      label: v.fieldDef.label,
      fieldType: v.fieldDef.fieldType,
      isRequired: v.fieldDef.isRequired,
      value: this.extractValue(v.fieldDef.fieldType as CustomFieldType, v),
    }));
  }

  // ============================================================
  // Integration-Helper fuer Entity-Detail-Responses
  // ============================================================

  /**
   * Liefert ALLE definierten Custom Fields fuer einen Entity-Typ,
   * zusammen mit den gespeicherten Werten (oder null).
   * Wird in contacts/companies/deals findOne() aufgerufen.
   */
  async getCustomFieldsForEntity(
    tenantId: string,
    entityType: CustomFieldEntityType,
    entityId: string,
  ): Promise<CustomFieldEntry[]> {
    // Alle Definitionen fuer diesen Entity-Typ laden
    const fieldDefs = await this.prisma.customFieldDef.findMany({
      where: { tenantId, entityType },
      orderBy: { position: 'asc' },
    });

    if (fieldDefs.length === 0) return [];

    // Alle Werte fuer diese Entity laden
    const values = await this.prisma.customFieldValue.findMany({
      where: {
        tenantId,
        entityId,
        fieldDefId: { in: fieldDefs.map((d) => d.id) },
      },
    });

    const valueMap = new Map(values.map((v) => [v.fieldDefId, v]));

    // Merge: ALLE Definitionen mit Wert (oder null)
    return fieldDefs.map((def) => {
      const valueRow = valueMap.get(def.id);
      return {
        fieldDefId: def.id,
        name: def.name,
        label: def.label,
        fieldType: def.fieldType,
        value: valueRow
          ? this.extractValue(def.fieldType as CustomFieldType, valueRow)
          : null,
      };
    });
  }

  // ============================================================
  // Value-Mapping: fieldType → Datenbank-Spalte
  // ============================================================

  private mapValueToColumns(
    fieldType: CustomFieldType,
    value: string | number | boolean | string[],
  ): {
    valueText: string | null;
    valueNumber: Prisma.Decimal | number | null;
    valueDate: Date | null;
    valueBoolean: boolean | null;
    valueJson: typeof Prisma.DbNull | Prisma.InputJsonValue;
  } {
    const result: {
      valueText: string | null;
      valueNumber: Prisma.Decimal | number | null;
      valueDate: Date | null;
      valueBoolean: boolean | null;
      valueJson: typeof Prisma.DbNull | Prisma.InputJsonValue;
    } = {
      valueText: null,
      valueNumber: null,
      valueDate: null,
      valueBoolean: null,
      valueJson: Prisma.DbNull, // Prisma erfordert DbNull statt null fuer JSON-Felder
    };

    switch (fieldType) {
      case CustomFieldType.TEXT:
      case CustomFieldType.TEXTAREA:
      case CustomFieldType.URL:
      case CustomFieldType.DROPDOWN:
        result.valueText = String(value);
        break;

      case CustomFieldType.NUMBER:
        result.valueNumber = Number(value);
        break;

      case CustomFieldType.DATE:
        result.valueDate = new Date(value as string);
        break;

      case CustomFieldType.CHECKBOX:
        result.valueBoolean = Boolean(value);
        break;

      case CustomFieldType.MULTI_SELECT:
        result.valueJson = value as Prisma.InputJsonValue;
        break;
    }

    return result;
  }

  // ============================================================
  // Value-Extraktion: Datenbank-Spalte → JS-Wert
  // ============================================================

  private extractValue(
    fieldType: CustomFieldType,
    row: {
      valueText: string | null;
      valueNumber: Prisma.Decimal | null;
      valueDate: Date | null;
      valueBoolean: boolean | null;
      valueJson: Prisma.JsonValue;
    },
  ): string | number | boolean | string[] | null {
    switch (fieldType) {
      case CustomFieldType.TEXT:
      case CustomFieldType.TEXTAREA:
      case CustomFieldType.URL:
      case CustomFieldType.DROPDOWN:
        return row.valueText;

      case CustomFieldType.NUMBER:
        return row.valueNumber !== null
          ? Number(row.valueNumber)
          : null;

      case CustomFieldType.DATE:
        return row.valueDate
          ? row.valueDate.toISOString()
          : null;

      case CustomFieldType.CHECKBOX:
        return row.valueBoolean;

      case CustomFieldType.MULTI_SELECT:
        return (row.valueJson as string[] | null) ?? null;

      default:
        return row.valueText;
    }
  }

  // ============================================================
  // Validierung
  // ============================================================

  /**
   * Prueft ob options fuer den gegebenen Feldtyp korrekt sind.
   */
  private validateOptionsForFieldType(
    fieldType: CustomFieldType,
    options?: Array<{ value: string; label: string }>,
  ): void {
    const isSelectType =
      fieldType === CustomFieldType.DROPDOWN ||
      fieldType === CustomFieldType.MULTI_SELECT;

    if (isSelectType && (!options || options.length === 0)) {
      throw new BadRequestException(
        'options ist Pflicht fuer DROPDOWN und MULTI_SELECT Felder',
      );
    }

    if (!isSelectType && options && options.length > 0) {
      throw new BadRequestException(
        'options darf nur fuer DROPDOWN/MULTI_SELECT gesetzt werden',
      );
    }
  }

  /**
   * Prueft ob ein Wert zum Feldtyp passt.
   */
  private validateValue(
    fieldType: CustomFieldType,
    label: string,
    options: Prisma.JsonValue,
    value: string | number | boolean | string[] | null,
  ): void {
    // null ist immer erlaubt (Feld leeren)
    if (value === null) return;

    switch (fieldType) {
      case CustomFieldType.TEXT:
      case CustomFieldType.TEXTAREA:
      case CustomFieldType.URL:
        if (typeof value !== 'string') {
          throw new BadRequestException(
            `Feld "${label}" erwartet einen String-Wert`,
          );
        }
        break;

      case CustomFieldType.NUMBER:
        if (typeof value !== 'number' && typeof value !== 'string') {
          throw new BadRequestException(
            `Feld "${label}" erwartet einen numerischen Wert`,
          );
        }
        if (isNaN(Number(value))) {
          throw new BadRequestException(
            `Feld "${label}": "${String(value)}" ist kein gueltiger numerischer Wert`,
          );
        }
        break;

      case CustomFieldType.DATE:
        if (typeof value !== 'string' || isNaN(Date.parse(value))) {
          throw new BadRequestException(
            `Feld "${label}" erwartet ein gueltiges Datum (ISO 8601)`,
          );
        }
        break;

      case CustomFieldType.CHECKBOX:
        if (typeof value !== 'boolean') {
          throw new BadRequestException(
            `Feld "${label}" erwartet einen Boolean-Wert`,
          );
        }
        break;

      case CustomFieldType.DROPDOWN: {
        if (typeof value !== 'string') {
          throw new BadRequestException(
            `Feld "${label}" erwartet einen String-Wert (Auswahl)`,
          );
        }
        const opts = options as Array<{ value: string; label: string }> | null;
        if (opts && !opts.some((o) => o.value === value)) {
          throw new BadRequestException(
            `Feld "${label}": "${value}" ist keine gueltige Option`,
          );
        }
        break;
      }

      case CustomFieldType.MULTI_SELECT: {
        if (!Array.isArray(value)) {
          throw new BadRequestException(
            `Feld "${label}" erwartet ein Array von Werten`,
          );
        }
        const msOpts = options as Array<{ value: string; label: string }> | null;
        if (msOpts) {
          const validValues = new Set(msOpts.map((o) => o.value));
          for (const v of value) {
            if (!validValues.has(v)) {
              throw new BadRequestException(
                `Feld "${label}": "${v}" ist keine gueltige Option`,
              );
            }
          }
        }
        break;
      }
    }
  }
}
