import { ForbiddenException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { PrismaService } from '../prisma/prisma.service';
import { CreateRecipeDto } from './dto/create-recipe.dto';
import { ParseMarkdownDto } from './dto/parse-markdown.dto';
import { downloadAndOptimizeImage } from '../common/utils/download-image';
import { normalizeUnit, getUnitType, convertUnit, canConvert } from '../common/utils/units';

const IMAGE_DEST_DIR = process.env.IMAGE_DEST_DIR || '/app/recipe-images';

// Lokala typdefiniitioner (tidigare från recipe-document-converter)
interface ParsedIngredient {
  rawName: string;
  quantity: number;
  unit: string;
  note: string | null;
}

interface ParsedRecipe {
  name: string;
  description: string;
  instructions: string;
  ingredients: ParsedIngredient[];
}

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

  constructor(private readonly prisma: PrismaService) {}

  async getInventoryPreview(id: number, userId: number) {
    const recipe = await this.prisma.recipe.findFirst({
      where: {
        id,
        OR: [
          { isPublic: true },
          { ownerId: userId },
          { shares: { some: { userId } } },
        ],
      },
      include: {
        ingredients: {
          include: {
            product: true,
          },
          orderBy: {
            id: 'asc',
          },
        },
      },
    });

    if (!recipe) {
      throw new NotFoundException(`Recipe with id ${id} not found`);
    }

    const ingredientPreviews = await Promise.all(
      recipe.ingredients.map(async (ingredient: any) => {
        const inventoryItems = await this.prisma.inventoryItem.findMany({
          where: { productId: ingredient.productId },
          orderBy: { createdAt: 'desc' },
        });

        // Hitta inventory-poster med samma enhet
        const sameUnitItems = inventoryItems.filter(
          (item: any) => item.unit.trim().toLowerCase() === ingredient.unit.trim().toLowerCase(),
        );
        const availableSameUnit = sameUnitItems.reduce(
          (sum: number, item: any) => sum + Number(item.quantity),
          0,
        );

        // Hitta inventory-poster med annan enhet och konvertera (endast viktbaserade enheter)
        const otherUnitItems = inventoryItems.filter(
          (item: any) => item.unit.trim().toLowerCase() !== ingredient.unit.trim().toLowerCase(),
        );
        let availableOtherUnit = 0;

        for (const item of otherUnitItems) {
          // Konvertera endast om enheter är kompatibla (samma kategori)
          try {
            const convertedQuantity = convertUnit(
              Number(item.quantity),
              item.unit,
              ingredient.unit,
            );
            availableOtherUnit += convertedQuantity;
          } catch {
            // Om konvertering misslyckas, hoppa över denna post
            // (t.ex. st kan inte konverteras till g)
          }
        }

        const totalAvailable = availableSameUnit + availableOtherUnit;
        let status: 'enough' | 'missing' | 'unit_mismatch';

        if (totalAvailable >= Number(ingredient.quantity)) {
          status = 'enough';
        } else if (availableSameUnit === 0 && availableOtherUnit > 0) {
          status = 'unit_mismatch';
        } else {
          status = 'missing';
        }

        return {
          ingredientId: ingredient.id,
          productId: ingredient.productId,
          productName: ingredient.product.canonicalName || ingredient.product.name,
          requiredQuantity: Number(ingredient.quantity),
          requiredUnit: ingredient.unit,
          note: ingredient.note,
          availableQuantity: totalAvailable,
          availableUnit: ingredient.unit,
          matchingInventoryItems: sameUnitItems.map((item: any) => ({
            id: item.id,
            quantity: item.quantity,
            unit: item.unit,
            location: item.location,
            brand: item.brand || null,
            bestBeforeDate: item.bestBeforeDate || null,
          })),
          otherInventoryItems: otherUnitItems.map((item: any) => {
              // Kolla om konvertering är möjlig (samma enhetskategori)
            const canConvertUnits = canConvert(item.unit, ingredient.unit);
            let convertedQuantity = 0;
            if (canConvertUnits) {
              try {
                convertedQuantity = convertUnit(Number(item.quantity), item.unit, ingredient.unit);
              } catch {
                convertedQuantity = 0;
              }
            }

            return {
              id: item.id,
              quantity: item.quantity,
              unit: item.unit,
              location: item.location,
              convertedQuantity: canConvertUnits ? convertedQuantity : 0,
              canConvert: canConvertUnits,
            };
          }),
          status,
          missingQuantity: status === 'missing' ? Math.max(0, Number(ingredient.quantity) - totalAvailable) : 0,
        };
      }),
    );

    const summary = {
      totalIngredients: ingredientPreviews.length,
      enoughCount: ingredientPreviews.filter((i: any) => i.status === 'enough').length,
      missingCount: ingredientPreviews.filter((i: any) => i.status === 'missing').length,
      unitMismatchCount: ingredientPreviews.filter((i: any) => i.status === 'unit_mismatch').length,
      canCookExactly: ingredientPreviews.every((i: any) => i.status === 'enough'),
    };

    return {
      recipe: {
        id: recipe.id,
        name: recipe.name,
        description: recipe.description,
      },
      ingredients: ingredientPreviews,
      summary,
    };
  }

  async findAll(userId: number) {
    return this.prisma.recipe.findMany({
      where: {
        OR: [
          { isPublic: true },
          { ownerId: userId },
          { shares: { some: { userId } } },
        ],
      },
      include: {
        ingredients: {
          include: {
            product: { include: { nutrition: true } },
          },
        },
        owner: { select: { id: true, username: true } },
        shares: { select: { userId: true } },
      },
    });
  }

  async findOne(id: number, userId: number) {
    const recipe = await this.prisma.recipe.findFirst({
      where: {
        id,
        OR: [
          { isPublic: true },
          { ownerId: userId },
          { shares: { some: { userId } } },
        ],
      },
      include: {
        ingredients: {
          include: {
            product: { include: { nutrition: true } },
          },
        },
        owner: { select: { id: true, username: true } },
        shares: { select: { userId: true } },
      },
    });

    if (!recipe) {
      throw new NotFoundException(`Recipe with id ${id} not found`);
    }

    return recipe;
  }

  async update(id: number, updateRecipeDto: CreateRecipeDto, userId: number) {
    // Verifiera att receptet finns och att användaren äger det
    const existingRecipe = await this.prisma.recipe.findUnique({
      where: { id },
    });

    if (!existingRecipe) {
      throw new NotFoundException(`Recipe with id ${id} not found`);
    }

    // Tillåt uppdatering om användaren är ägare ELLER om receptet är publikt utan ägare
    if (existingRecipe.ownerId !== null && existingRecipe.ownerId !== userId) {
      throw new NotFoundException(`Recipe with id ${id} not found`);
    }

    // Ta bort gamla ingredienser
    await this.prisma.recipeIngredient.deleteMany({
      where: { recipeId: id },
    });

    // Uppdatera receptet och lägg till nya ingredienser
    const recipe = await this.prisma.recipe.update({
      where: { id },
      data: {
        name: updateRecipeDto.name,
        description: updateRecipeDto.description || null,
        instructions: updateRecipeDto.instructions || null,
        servings: updateRecipeDto.servings ?? null,
        ...(updateRecipeDto.isPublic !== undefined && { isPublic: updateRecipeDto.isPublic }),
        ...(updateRecipeDto.imageUrl !== undefined && { imageUrl: updateRecipeDto.imageUrl || null }),
        ingredients: {
          create: updateRecipeDto.ingredients.map((ingredient) => ({
            productId: ingredient.productId,
            quantity: ingredient.quantity,
            unit: ingredient.unit,
            note: ingredient.note || null,
          })),
        },
      },
      include: {
        ingredients: {
          include: {
            product: { include: { nutrition: true } },
          },
        },
      },
    });

    return recipe;
  }

  async remove(id: number, userId: number) {
    const existingRecipe = await this.prisma.recipe.findUnique({
      where: { id },
    });

    if (!existingRecipe) {
      throw new NotFoundException(`Recipe with id ${id} not found`);
    }

    if (existingRecipe.ownerId !== null && existingRecipe.ownerId !== userId) {
      throw new NotFoundException(`Recipe with id ${id} not found`);
    }

    await this.prisma.recipeIngredient.deleteMany({ where: { recipeId: id } });
    await this.prisma.recipe.delete({ where: { id } });

    // Radera lokal bildfil om den finns (undviker orphan-filer på disk).
    if (existingRecipe.imageUrl?.startsWith('/images/')) {
      const filename = path.basename(existingRecipe.imageUrl);
      const filePath = path.join(IMAGE_DEST_DIR, filename);
      await fs.unlink(filePath).catch(() => {
        // Filen kanske redan är borttagen — ignorera felet.
      });
    }
  }

  async updateImage(id: number, sourceUrl: string, userId: number) {
    const existingRecipe = await this.prisma.recipe.findUnique({ where: { id } });
    if (!existingRecipe) {
      throw new NotFoundException(`Recipe with id ${id} not found`);
    }
    if (existingRecipe.ownerId !== userId) {
      throw new NotFoundException(`Recipe with id ${id} not found`);
    }

    const imageUrl = await downloadAndOptimizeImage(sourceUrl, IMAGE_DEST_DIR);

    return this.prisma.recipe.update({
      where: { id },
      data: { imageUrl },
      include: {
        ingredients: { include: { product: { include: { nutrition: true } } } },
        owner: { select: { id: true, username: true } },
        shares: { select: { userId: true } },
      },
    });
  }

  async setVisibility(id: number, userId: number, isPublic: boolean) {
    const existingRecipe = await this.prisma.recipe.findUnique({ where: { id } });
    if (!existingRecipe || existingRecipe.ownerId !== userId) {
      throw new NotFoundException(`Recipe with id ${id} not found`);
    }

    if (isPublic) {
      const owner = await this.prisma.user.findUnique({
        where: { id: userId },
        select: { canShareRecipes: true },
      });
      if (!owner?.canShareRecipes) {
        throw new ForbiddenException('Du har inte behörighet att dela recept.');
      }
    }

    return this.prisma.recipe.update({
      where: { id },
      data: { isPublic },
      include: {
        ingredients: { include: { product: { include: { nutrition: true } } } },
        owner: { select: { id: true, username: true } },
        shares: { select: { userId: true } },
      },
    });
  }

  async shareWithUser(id: number, ownerId: number, username: string) {
    const recipe = await this.prisma.recipe.findUnique({
      where: { id },
      select: { id: true, ownerId: true },
    });
    if (!recipe || recipe.ownerId !== ownerId) {
      throw new NotFoundException(`Recipe with id ${id} not found`);
    }

    const owner = await this.prisma.user.findUnique({
      where: { id: ownerId },
      select: { canShareRecipes: true },
    });
    if (!owner?.canShareRecipes) {
      throw new ForbiddenException('Du har inte behörighet att dela recept.');
    }

    const targetUser = await this.prisma.user.findUnique({
      where: { username },
      select: { id: true },
    });
    if (!targetUser) {
      throw new NotFoundException(`User ${username} not found`);
    }
    if (targetUser.id === ownerId) {
      return this.findOne(id, ownerId);
    }

    await this.prisma.recipeShare.upsert({
      where: { recipeId_userId: { recipeId: id, userId: targetUser.id } },
      create: { recipeId: id, userId: targetUser.id },
      update: {},
    });

    return this.findOne(id, ownerId);
  }

  async unshareWithUser(id: number, ownerId: number, username: string) {
    const recipe = await this.prisma.recipe.findUnique({
      where: { id },
      select: { id: true, ownerId: true },
    });
    if (!recipe || recipe.ownerId !== ownerId) {
      throw new NotFoundException(`Recipe with id ${id} not found`);
    }

    const targetUser = await this.prisma.user.findUnique({
      where: { username },
      select: { id: true },
    });
    if (!targetUser) {
      throw new NotFoundException(`User ${username} not found`);
    }

    await this.prisma.recipeShare.deleteMany({
      where: { recipeId: id, userId: targetUser.id },
    });

    return this.findOne(id, ownerId);
  }

  async create(createRecipeDto: CreateRecipeDto, userId: number) {
    this.logger.log(
      `[create] Incoming imageUrl from client: ${createRecipeDto.imageUrl ?? 'null'}`,
    );

    // Om imageUrl är en extern URL — ladda ner och optimera
    let imageUrl: string | null = createRecipeDto.imageUrl || null;
    if (imageUrl && imageUrl.startsWith('http')) {
      const externalImageUrl = imageUrl;
      try {
        imageUrl = await downloadAndOptimizeImage(imageUrl, IMAGE_DEST_DIR);
      } catch (err) {
        console.warn('[RecipesService] Kunde inte ladda ner receptbild:', err);
        // Behåll extern URL som fallback så bild fortfarande visas.
        imageUrl = externalImageUrl;
      }
    }

    this.logger.log(`[create] Final imageUrl persisted to DB: ${imageUrl ?? 'null'}`);

    const recipe = await this.prisma.recipe.create({
      data: {
        name: createRecipeDto.name,
        description: createRecipeDto.description || null,
        instructions: createRecipeDto.instructions || null,
        imageUrl,
        servings: createRecipeDto.servings ?? null,
        ownerId: userId,
        isPublic: false,
        ingredients: {
          create: createRecipeDto.ingredients.map((ingredient) => ({
            productId: ingredient.productId,
            quantity: ingredient.quantity,
            unit: ingredient.unit,
            note: ingredient.note || null,
          })),
        },
      },
      include: {
        ingredients: {
          include: {
            product: { include: { nutrition: true } },
          },
        },
      },
    });

    return recipe;
  }

  async parseMarkdown(dto: ParseMarkdownDto) {
    // Delegera markdown-parsning till microservice-importer
    const importerUrl = process.env.IMPORTER_SERVICE_URL || 'http://importer-api:3001';
    let parsed: ParsedRecipe;
    try {
      const response = await fetch(`${importerUrl}/api/recipes/parse-markdown`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ markdown: dto.markdown }),
      });
      if (!response.ok) {
        throw new Error(`Importer svarade ${response.status}`);
      }
      parsed = (await response.json()) as ParsedRecipe;
    } catch (err) {
      this.logger.error(`Kunde inte nå importer-api för parse-markdown: ${err}`);
      // Fallback: använd lokal parser vid driftavbrott
      parsed = parseRecipeMarkdown(dto.markdown);
    }

    const allProducts = await this.prisma.product.findMany({
      where: { isActive: true },
      select: { id: true, name: true, canonicalName: true, normalizedName: true },
    });

    // Normalisera en sträng för jämförelse (lowercase, trim, ta bort skiljetecken)
    const normalize = (s: string) =>
      s.toLowerCase().trim().replace(/[^a-zåäö0-9\s]/gi, '').replace(/\s+/g, ' ');

    // Enkel Levenshtein-distans
    const levenshtein = (a: string, b: string): number => {
      const m = a.length;
      const n = b.length;
      const dp: number[][] = Array.from({ length: m + 1 }, (_, i) =>
        Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
      );
      for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
          dp[i][j] =
            a[i - 1] === b[j - 1]
              ? dp[i - 1][j - 1]
              : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
        }
      }
      return dp[m][n];
    };

    const ingredientsWithSuggestions = parsed.ingredients.map((ingredient: ParsedIngredient) => {
      const query = normalize(ingredient.rawName);

      const scored = allProducts
        .map((product) => {
          const targetName = normalize(product.canonicalName || product.name);
          const targetNormalized = normalize(product.normalizedName);

          // Exakt träff på normalizedName prioriteras
          if (targetNormalized === query || targetName === query) {
            return { product, score: 100 };
          }

          // Delsträng-match
          if (targetName.includes(query) || query.includes(targetName)) {
            return { product, score: 70 };
          }

          // Levenshtein-baserad likhet
          const dist = levenshtein(query, targetName);
          const maxLen = Math.max(query.length, targetName.length);
          const similarity = maxLen === 0 ? 100 : Math.round((1 - dist / maxLen) * 100);

          return { product, score: similarity };
        })
        .filter((s) => s.score >= 40)
        .sort((a, b) => b.score - a.score)
        .slice(0, 5)
        .map((s) => ({
          productId: s.product.id,
          productName: s.product.canonicalName || s.product.name,
          score: s.score,
        }));

      return {
        rawName: ingredient.rawName,
        quantity: ingredient.quantity,
        unit: ingredient.unit,
        note: ingredient.note,
        suggestions: scored,
      };
    });

    return {
      name: parsed.name,
      description: parsed.description,
      instructions: parsed.instructions,
      ingredients: ingredientsWithSuggestions,
    };
  }
}

// ============================================================================
// Parser Functions (previously from recipe-document-converter library)
// ============================================================================

/**
 * Parsar ett recept i Markdown-format och extraherar namn, beskrivning,
 * instruktioner och ingredienser.
 *
 * Förväntat format:
 *   # Receptnamn
 *   Beskrivning (valfritt stycke efter titeln)
 *
 *   ## Ingredienser
 *   - 400 g kycklingfilé
 *   - 2 dl grädde (eller crème fraiche)
 *
 *   ## Instruktioner
 *   1. Stek kycklingen …
 */
function parseRecipeMarkdown(markdown: string): ParsedRecipe {
  const lines = markdown.split('\n');

  let name = '';
  let description = '';
  let instructions = '';
  const ingredients: ParsedIngredient[] = [];

  let currentSection: 'none' | 'description' | 'ingredients' | 'instructions' = 'none';
  const descriptionLines: string[] = [];
  const instructionLines: string[] = [];

  for (const line of lines) {
    const trimmed = line.trim();

    // H1 — receptnamn
    if (/^#\s+/.test(trimmed) && !trimmed.startsWith('##')) {
      name = trimmed.replace(/^#\s+/, '').trim();
      currentSection = 'description';
      continue;
    }

    // H2 — sektionsrubriker
    if (/^##\s+/.test(trimmed)) {
      const heading = trimmed.replace(/^##\s+/, '').trim().toLowerCase();
      if (/ingrediens/.test(heading)) {
        currentSection = 'ingredients';
      } else if (/instruktion|tillagning|gör så här|steg|tillväg|metod/.test(heading)) {
        currentSection = 'instructions';
      } else {
        currentSection = 'none';
      }
      continue;
    }

    // Samla rader beroende på sektion
    switch (currentSection) {
      case 'description':
        if (trimmed.length > 0) {
          descriptionLines.push(trimmed);
        }
        break;

      case 'ingredients':
        if (/^[-*]\s+/.test(trimmed)) {
          const ingredientText = trimmed.replace(/^[-*]\s+/, '');
          ingredients.push(parseIngredientLine(ingredientText));
        }
        break;

      case 'instructions':
        if (trimmed.length > 0) {
          instructionLines.push(trimmed);
        }
        break;
    }
  }

  description = descriptionLines.join('\n');
  instructions = instructionLines.join('\n');

  return { name, description, instructions, ingredients };
}

/**
 * Parsar en ingrediensrad, t.ex.:
 *   "400 g kycklingfilé"
 *   "2 dl grädde (eller crème fraiche)"
 *   "1 1/2 dl crème fraiche"
 *   "1 polka- eller gulbeta"
 *   "1 kruka basilika"
 *   "salt"
 */
function parseIngredientLine(text: string): ParsedIngredient {
  const trimmed = text.trim();

  // Kända enheter
  const knownUnits = [
    'g', 'kg', 'hg', 'mg', 'ml', 'dl', 'l', 'tl',
    'st', 'tsk', 'msk', 'krm', 'matsled', 'tesled',
    'pris', 'portion', 'port', 'burk', 'förp', 'paket', 'efter smak', 'klyfta',
  ];

  // Extrahera eventuell parentes-not i slutet
  let note: string | null = null;
  let main = trimmed;
  const parenMatch = trimmed.match(/\(([^)]+)\)\s*$/);
  if (parenMatch) {
    note = parenMatch[1].trim();
    main = trimmed.slice(0, parenMatch.index).trim();
  }

  // Försök matcha bråk först: "1 1/2 dl crème fraiche" eller "1/2 dl"
  const fractionMatch = main.match(/^(\d+)?\s*(\d+)\s*\/\s*([\d.]+)\s+(\S+)\s+(.*)$/);
  if (fractionMatch) {
    let quantity = 0;
    if (fractionMatch[1]) {
      quantity = parseFloat(fractionMatch[1]) + parseFloat(fractionMatch[2]) / parseFloat(fractionMatch[3]);
    } else {
      quantity = parseFloat(fractionMatch[2]) / parseFloat(fractionMatch[3]);
    }
    const candidateUnit = fractionMatch[4].toLowerCase();
    if (knownUnits.includes(candidateUnit)) {
      return {
        quantity,
        unit: candidateUnit,
        rawName: fractionMatch[5].trim(),
        note,
      };
    }
  }

  // Försök matcha "kvantitet enhet namn" — t.ex. "400 g kycklingfilé" eller "2.5 dl grädde"
  const fullMatch = main.match(/^(\d+(?:[.,]\d+)?)\s+(\S+)\s+(.+)$/);
  if (fullMatch) {
    const candidateUnit = fullMatch[2].toLowerCase();
    // Validera att det andra ordet är en känd enhet
    if (knownUnits.includes(candidateUnit)) {
      return {
        quantity: parseNumber(fullMatch[1]),
        unit: candidateUnit,
        rawName: fullMatch[3].trim(),
        note,
      };
    }
    // Om inte känd enhet, behandla som "kvantitet namn" utan enhet
    return {
      quantity: parseNumber(fullMatch[1]),
      unit: 'st',
      rawName: fullMatch[2] + ' ' + fullMatch[3],
      note,
    };
  }

  // Försök matcha "kvantitet namn" utan enhet — t.ex. "3 ägg"
  const noUnitMatch = main.match(/^(\d+(?:[.,]\d+)?)\s+(.+)$/);
  if (noUnitMatch) {
    return {
      quantity: parseNumber(noUnitMatch[1]),
      unit: 'st',
      rawName: noUnitMatch[2].trim(),
      note,
    };
  }

  // Bara ett namn, ingen kvantitet — t.ex. "salt"
  return {
    quantity: 0,
    unit: '',
    rawName: main,
    note,
  };
}

function parseNumber(s: string): number {
  return parseFloat(s.replace(',', '.'));
}