import type { Database, SqlValue } from 'sql.js';
import type { Recipe, CreateRecipeInput, UpdateRecipeInput, RecipeFilters, Ingredient, Step } from '../types/recipe.js';
import type { Tag } from '../types/tag.js';

export class RecipeRepository {
  constructor(private db: Database) {}

  private toNullableSql(value: string | null | undefined): SqlValue {
    return value ?? null;
  }

  private toNullableUrl(value: string | null | undefined): string | null {
    if (value === undefined || value === null) return null;
    const trimmed = value.trim();
    return trimmed.length ? trimmed : null;
  }

  private toRequiredSqlText(value: string | null | undefined, fieldName: string): string {
    if (value === undefined || value === null || value.trim() === '') {
      throw new Error(`${fieldName} is required`);
    }
    return value;
  }

  findAll(filters: RecipeFilters = {}): Recipe[] {
    const { search, tagId, offset = 0, limit = 50 } = filters as any;
    let sql = `SELECT DISTINCT r.* FROM recipes r 
      LEFT JOIN ingredients i ON r.id = i.recipe_id 
      LEFT JOIN recipe_tags rt ON r.id = rt.recipe_id 
      LEFT JOIN tags t ON rt.tag_id = t.id`;
    const clauses: string[] = [];
    const params: SqlValue[] = [];
    
    if (search) {
      const s = `%${search}%`;
      clauses.push(`(r.title LIKE ? OR r.description LIKE ? OR i.item LIKE ? OR t.name LIKE ?)`);
      params.push(s, s, s, s);
    }
    if (tagId !== undefined && tagId !== null) {
      clauses.push('rt.tag_id = ?');
      params.push(tagId);
    }
    if (clauses.length > 0) {
      sql += ' WHERE ' + clauses.join(' AND ');
    }
    sql += ' ORDER BY r.created_at DESC LIMIT ? OFFSET ?';
    params.push(limit, offset);
    const result = this.db.exec(sql, params);
    if (!result.length) return [];
    return result[0].values.map(row => this.assembleRecipe(row, result[0].columns));
  }

  findById(id: number): Recipe | null {
    const result = this.db.exec('SELECT * FROM recipes WHERE id = ?', [id]);
    if (!result.length || !result[0].values.length) return null;
    return this.assembleRecipe(result[0].values[0], result[0].columns);
  }

  create(input: CreateRecipeInput): Recipe {
    const now = Math.floor(Date.now() / 1000);
    let recipeId: number;
    
    // Begin transaction
    this.db.run('BEGIN TRANSACTION');
    try {
      this.db.run(
        `INSERT INTO recipes (title, description, servings, prep_time_minutes, cook_time_minutes, source_url, image_url, made, rating, notes, created_at, updated_at)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
        [
          input.title,
          input.description ?? null,
          input.servings ?? null,
          input.prep_time_minutes ?? null,
          input.cook_time_minutes ?? null,
          this.toNullableUrl(input.source_url),
          this.toNullableUrl(input.image_url),
          input.made ? 1 : 0,
          input.rating ?? null,
          input.notes ?? null,
          now,
          now
        ]
      );
      const idResult = this.db.exec('SELECT last_insert_rowid() as id');
      if (!idResult.length || !idResult[0].values.length) {
        throw new Error('Failed to get recipe ID');
      }
      recipeId = idResult[0].values[0][0] as number;
      
      if (input.ingredients) {
        input.ingredients.forEach((ing, i) => {
          this.db.run('INSERT INTO ingredients (recipe_id, position, quantity, unit, item, notes) VALUES (?, ?, ?, ?, ?, ?)',
            [
              recipeId,
              i,
              this.toNullableSql(ing.quantity),
              this.toNullableSql(ing.unit),
              this.toRequiredSqlText(ing.item, 'ingredient.item'),
              this.toNullableSql(ing.notes)
            ]);
        });
      }
      if (input.steps) {
        input.steps.forEach((step, i) => {
          this.db.run('INSERT INTO steps (recipe_id, position, instruction) VALUES (?, ?, ?)',
            [recipeId, i, this.toRequiredSqlText(step.instruction, 'step.instruction')]);
        });
      }
      if (input.tagIds && input.tagIds.length > 0) {
        input.tagIds.forEach(tagId => {
          this.db.run('INSERT INTO recipe_tags (recipe_id, tag_id) VALUES (?, ?)', [recipeId, tagId]);
        });
      }
      
      // Commit transaction
      this.db.run('COMMIT');
    } catch (error) {
      // Rollback on error
      this.db.run('ROLLBACK');
      throw error;
    }
    
    return this.findById(recipeId)!;
  }

  update(id: number, input: UpdateRecipeInput): Recipe | null {
    const existing = this.findById(id);
    if (!existing) return null;
    const now = Math.floor(Date.now() / 1000);
    
    // Begin transaction
    this.db.run('BEGIN TRANSACTION');
    try {
      const fields: string[] = [];
      const params: SqlValue[] = [];
      if (input.title !== undefined) { fields.push('title = ?'); params.push(input.title); }
      if (input.description !== undefined) { fields.push('description = ?'); params.push(input.description); }
      if (input.servings !== undefined) { fields.push('servings = ?'); params.push(input.servings); }
      if (input.prep_time_minutes !== undefined) { fields.push('prep_time_minutes = ?'); params.push(input.prep_time_minutes); }
      if (input.cook_time_minutes !== undefined) { fields.push('cook_time_minutes = ?'); params.push(input.cook_time_minutes); }
      if (input.source_url !== undefined) { fields.push('source_url = ?'); params.push(this.toNullableUrl(input.source_url)); }
      if (input.image_url !== undefined) { fields.push('image_url = ?'); params.push(this.toNullableUrl(input.image_url)); }
      if (input.made !== undefined) { fields.push('made = ?'); params.push(input.made ? 1 : 0); }
      if (input.rating !== undefined) { fields.push('rating = ?'); params.push(input.rating); }
      if (input.notes !== undefined) { fields.push('notes = ?'); params.push(input.notes); }
      fields.push('updated_at = ?'); params.push(now);
      params.push(id);
      this.db.run(`UPDATE recipes SET ${fields.join(', ')} WHERE id = ?`, params);
      
      if (input.ingredients !== undefined) {
        this.db.run('DELETE FROM ingredients WHERE recipe_id = ?', [id]);
        input.ingredients.forEach((ing, i) => {
          this.db.run('INSERT INTO ingredients (recipe_id, position, quantity, unit, item, notes) VALUES (?, ?, ?, ?, ?, ?)',
            [
              id,
              i,
              this.toNullableSql(ing.quantity),
              this.toNullableSql(ing.unit),
              this.toRequiredSqlText(ing.item, 'ingredient.item'),
              this.toNullableSql(ing.notes)
            ]);
        });
      }
      if (input.steps !== undefined) {
        this.db.run('DELETE FROM steps WHERE recipe_id = ?', [id]);
        input.steps.forEach((step, i) => {
          this.db.run('INSERT INTO steps (recipe_id, position, instruction) VALUES (?, ?, ?)', [id, i, this.toRequiredSqlText(step.instruction, 'step.instruction')]);
        });
      }
      if (input.tagIds !== undefined) {
        this.db.run('DELETE FROM recipe_tags WHERE recipe_id = ?', [id]);
        input.tagIds.forEach(tagId => {
          this.db.run('INSERT INTO recipe_tags (recipe_id, tag_id) VALUES (?, ?)', [id, tagId]);
        });
      }
      
      // Commit transaction
      this.db.run('COMMIT');
    } catch (error) {
      this.db.run('ROLLBACK');
      throw error;
    }
    
    return this.findById(id);
  }

  delete(id: number): boolean {
    const existing = this.findById(id);
    if (!existing) return false;
    this.db.run('DELETE FROM recipes WHERE id = ?', [id]);
    return true;
  }

  count(filters: RecipeFilters = {}): number {
    const { search, tagId } = filters as any;
    let sql = `SELECT COUNT(DISTINCT r.id) as count FROM recipes r 
      LEFT JOIN ingredients i ON r.id = i.recipe_id 
      LEFT JOIN recipe_tags rt ON r.id = rt.recipe_id 
      LEFT JOIN tags t ON rt.tag_id = t.id`;
    const clauses: string[] = [];
    const params: SqlValue[] = [];
    if (search) {
      const s = `%${search}%`;
      clauses.push("(r.title LIKE ? OR r.description LIKE ? OR i.item LIKE ? OR t.name LIKE ?)");
      params.push(s, s, s, s);
    }
    if (tagId !== undefined && tagId !== null) {
      clauses.push('rt.tag_id = ?');
      params.push(tagId);
    }
    if (clauses.length > 0) {
      sql += ' WHERE ' + clauses.join(' AND ');
    }
    const result = this.db.exec(sql, params);
    return result[0].values[0][0] as number;
  }

  private assembleRecipe(row: SqlValue[], columns: string[]): Recipe {
    const map: Record<string, SqlValue> = {};
    columns.forEach((col, idx) => { map[col] = row[idx]; });
    const id = map.id as number;
    const ingredientsRes = this.db.exec('SELECT * FROM ingredients WHERE recipe_id = ? ORDER BY position ASC', [id]);
    const ingredients: Ingredient[] = ingredientsRes.length ?
      ingredientsRes[0].values.map(r => ({
        id: r[0] as number,
        recipe_id: r[1] as number,
        position: r[2] as number,
        quantity: typeof r[3] === 'undefined' ? null : r[3] as string,
        unit: typeof r[4] === 'undefined' ? null : r[4] as string,
        item: r[5] as string,
        notes: typeof r[6] === 'undefined' ? null : r[6] as string }))
      : [];
    const stepsRes = this.db.exec('SELECT * FROM steps WHERE recipe_id = ? ORDER BY position ASC', [id]);
    const steps: Step[] = stepsRes.length ?
      stepsRes[0].values.map(r => ({
        id: r[0] as number,
        recipe_id: r[1] as number,
        position: r[2] as number,
        instruction: r[3] as string
      })) : [];
    const tagsRes = this.db.exec('SELECT t.* FROM tags t INNER JOIN recipe_tags rt ON rt.tag_id = t.id WHERE rt.recipe_id = ?', [id]);
    const tags: Tag[] = tagsRes.length ? tagsRes[0].values.map(r => ({ id: r[0] as number, name: r[1] as string })) : [];
    return {
      id,
      title: map.title as string,
      description: map.description as string | null,
      servings: map.servings as number | null,
      prep_time_minutes: map.prep_time_minutes as number | null,
      cook_time_minutes: map.cook_time_minutes as number | null,
      source_url: map.source_url as string | null,
      image_url: (map.image_url as string | null) ?? null,
      made: Boolean(map.made),
      rating: map.rating as number | null,
      notes: map.notes as string | null,
      created_at: map.created_at as number,
      updated_at: map.updated_at as number,
      ingredients,
      steps,
      tags
    };
  }
}
