import {
  PrismaClient,
  User,
  Prisma,
  VillageObjectType,
  VillageTile,
} from '@prisma/client';
import { COSTS, REWARDS } from '../utils/economy';
import { applyStreakMultiplier } from '../utils/streak';
import { getTodayDay, isBeforeDay, daysSince } from '../utils/gameDay';
import { calculateDailyStreak } from '../utils/streak';

const prisma = new PrismaClient();

/* =========================
   CONSTANTS
========================= */

export const VILLAGE_WIDTH = 5;
export const VILLAGE_HEIGHT = 7;

export const PRODUCING_BUILDINGS = [
  'FIELD',
  'LUMBERJACK',
  'QUARRY',
] as const;

/* =========================
   TYPES
========================= */

type FullVillage = Prisma.VillageGetPayload<{
  include: {
    user: true;
    tiles: { include: { object: true } };
    objects: { include: { tile: true } };
  };
}>;

/* =========================
   PUBLIC API
========================= */

/**
 * Processes the village's daily "tick" if necessary and returns the
 * complete, up-to-date village state.
 * This is the single source of truth for all time-based progression.
 */
export async function processVillageTick(userId: number): Promise<FullVillage> {
  try {
    const today = getTodayDay();
    let villageSnapshot = await fetchVillage(userId);

    if (!villageSnapshot) {
      // This should not happen for a logged-in user with a village.
      throw createError({ statusCode: 404, statusMessage: 'Village not found' });
    }

    // Even if tick is done, we should ensure the streak is updated for the day.
    // The calculateDailyStreak function is idempotent.
    if (villageSnapshot.lastTickDay === today) {
      villageSnapshot.user = await calculateDailyStreak(prisma, userId, today);
      return villageSnapshot;
    }

    // The tick for today has not run. Execute all daily logic in a transaction.
    await prisma.$transaction(async (tx) => {
      // 1. UPDATE STREAK FIRST. This is critical for all reward calculations.
      const updatedUser = await calculateDailyStreak(tx, userId, today);
      villageSnapshot.user = updatedUser; // Update snapshot with fresh user data.

      // 2. Process other daily logic using the updated snapshot
      const finishedTiles = await processFinishedClearing(tx, villageSnapshot, today);
      await processFieldExp(tx, villageSnapshot, today);
      await autoStartClearing(tx, villageSnapshot, today, finishedTiles);
      
      // 3. Update the last tick day to prevent re-processing
      await tx.village.update({
        where: { id: villageSnapshot.id },
        data: { lastTickDay: today },
      });
    });

    // After the transaction, the original villageSnapshot is stale.
    // Re-fetch to get the latest state with all changes.
    const updatedVillage = await fetchVillage(userId);
    if (!updatedVillage) {
      // This would be a critical error, as the village existed moments ago.
      throw createError({ statusCode: 500, statusMessage: 'Village disappeared post-transaction' });
    }
    
    return updatedVillage;

  } catch (error) {
    // Log the error and re-throw it to be handled by the calling API endpoint.
    console.error(`Error in processVillageTick for user ${userId}:`, error);
    if ((error as any).statusCode) throw error; // Re-throw h3 errors
    throw createError({ statusCode: 500, statusMessage: 'Failed to process village tick.' });
  }
}

/**
 * Main entry point for the frontend to get the village state.
 * It ensures the daily tick is processed, then enriches the state with UI-specific data.
 */
export async function syncAndGetVillage(userId: number): Promise<FullVillage> {
  try {
    // This function will now run the tick (if needed) AND return the up-to-date village state.
    const villageSnapshot = await processVillageTick(userId);

    // --- Enrich tiles with available actions ---
    const user = villageSnapshot.user;
    const housesCount = villageSnapshot.objects.filter(o => o.type === 'HOUSE').length;
    const producingCount = villageSnapshot.objects.filter(o => PRODUCING_BUILDINGS.includes(o.type as any)).length;
    const freeWorkers = housesCount - producingCount;

    const tilesWithActions = villageSnapshot.tiles.map(tile => {
      const availableActions: any[] = [];

      // Action: BUILD
      if (tile.terrainType === 'EMPTY' && !tile.object) {
        const buildableObjectTypes: VillageObjectType[] = ['HOUSE', 'FIELD', 'LUMBERJACK', 'QUARRY', 'WELL'];
        const buildActions = buildableObjectTypes.map(buildingType => {
          const cost = COSTS.BUILD[buildingType];
          const isProducing = (PRODUCING_BUILDINGS as readonly string[]).includes(buildingType);
          let isEnabled = user.coins >= cost;
          let disabledReason = user.coins < cost ? 'Not enough coins' : undefined;

          if (isEnabled && isProducing && freeWorkers <= 0) {
            isEnabled = false;
            disabledReason = 'Not enough workers';
          }
          
          return {
            type: 'BUILD',
            buildingType,
            cost,
            isEnabled,
            disabledReason,
          };
        });
        availableActions.push(...buildActions);
      }
      
      return { ...tile, availableActions };
    });

    return { ...villageSnapshot, tiles: tilesWithActions } as FullVillage;
  } catch (error) {
    console.error('Error in syncAndGetVillage:', error);
    // Let the API endpoint handle the final error response.
    if ((error as any).statusCode) throw error; // Re-throw h3 errors
    throw createError({ statusCode: 500, statusMessage: 'An error occurred during village synchronization.' });
  }
}

/**
 * Генерация деревни для нового пользователя
 */
export async function generateVillageForUser(user: User) {
  // Enforce immutability of the village layout and ensure creation occurs only once.
  await prisma.$transaction(async (tx) => {
    // 1. Find or create Village
    let village = await tx.village.findUnique({
      where: { userId: user.id },
    });

    let villageCreated = false;
    if (!village) {
      village = await tx.village.create({
        data: { userId: user.id },
      });
      villageCreated = true;
    }

    // If village was just created, initialize user resources
    if (villageCreated) {
      await tx.user.update({
        where: { id: user.id },
        data: { coins: 10, exp: 0 },
      });
    }

    // 2. Count existing VillageTiles for this Village
    const tilesCount = await tx.villageTile.count({
      where: { villageId: village!.id }, // village is guaranteed to exist here
    });

    // If tiles already exist, layout is immutable. Do nothing.
    if (tilesCount > 0) {
      // Village layout is immutable once created.
      return;
    }

    // 3. Create tiles ONLY if tilesCount is 0 (broken state or first creation)
    // This logic ensures tiles are created exactly once.
    const tilesToCreate: Omit<
      VillageTile,
      'id' | 'clearingStartedDay' | 'villageId'
    >[] = [];

    const centralXStart = 1;
    const centralXEnd = 4;
    const centralYStart = 2;
    const centralYEnd = 5;

    for (let y = 0; y < VILLAGE_HEIGHT; y++) {
      for (let x = 0; x < VILLAGE_WIDTH; x++) {
        const isCentral =
          x >= centralXStart &&
          x < centralXEnd &&
          y >= centralYStart &&
          y < centralYEnd;

        tilesToCreate.push({
          x,
          y,
          terrainType: isCentral
            ? 'EMPTY'
            : Math.random() < 0.5
            ? 'BLOCKED_TREE'
            : 'BLOCKED_STONE',
          terrainState: 'IDLE',
        });
      }
    }

    await tx.villageTile.createMany({
      data: tilesToCreate.map((t) => ({
        ...t,
        villageId: village!.id, // village is guaranteed to exist here
      })),
    });
  });
}

/**
 * BUILD command
 */
export async function buildOnTile(
  userId: number,
  tileId: number,
  buildingType: VillageObjectType
) {
  return prisma.$transaction(async (tx) => {
    const user = await tx.user.findUniqueOrThrow({
      where: { id: userId },
    });

    const tile = await tx.villageTile.findUniqueOrThrow({
      where: { id: tileId },
      include: { village: true },
    });

    if (tile.village.userId !== userId) {
      throw createError({ statusCode: 403, statusMessage: 'Not your tile' });
    }

    if (tile.terrainType !== 'EMPTY' || tile.object) {
      throw createError({ statusCode: 400, statusMessage: 'Tile is not empty' });
    }

    const cost = COSTS.BUILD[buildingType];
    if (user.coins < cost) {
      throw createError({ statusCode: 400, statusMessage: 'Not enough coins' });
    }

    if (PRODUCING_BUILDINGS.includes(buildingType as any)) {
      const objects = await tx.villageObject.findMany({
        where: { villageId: tile.villageId },
      });

      const houses = objects.filter(o => o.type === 'HOUSE').length;
      const producing = objects.filter(o =>
        PRODUCING_BUILDINGS.includes(o.type as any)
      ).length;

      if (producing >= houses) {
        throw createError({
          statusCode: 400,
          statusMessage: 'Not enough workers',
        });
      }
    }

    await tx.user.update({
      where: { id: userId },
      data: { coins: { decrement: cost } },
    });

    await tx.villageObject.create({
      data: {
        type: buildingType,
        villageId: tile.villageId,
        tileId: tile.id,
      },
    });

    await tx.villageEvent.create({
      data: {
        villageId: tile.villageId,
        type: `BUILD_${buildingType}`,
        message: `Построено ${buildingType} на (${tile.x}, ${tile.y})`,
        tileX: tile.x,
        tileY: tile.y,
        coins: -cost,
        exp: 0,
      },
    });

    // If a clearing building was built, immediately try to start a new clearing job.
    // This makes new buildings feel responsive and start working right away.
    if (buildingType === 'LUMBERJACK' || buildingType === 'QUARRY') {
      const today = getTodayDay();
      // We need a fresh, full snapshot of the village *within the transaction*
      // to correctly calculate clearing capacity.
      const villageSnapshot = await tx.village.findUnique({
        where: { id: tile.villageId },
        include: {
          user: true,
          tiles: { include: { object: true } },
          objects: { include: { tile: true } },
        },
      });

      if (villageSnapshot) {
        await autoStartClearing(tx, villageSnapshot, today);
      }
    }
  });
}

/* =========================
   INTERNAL HELPERS
========================= */

function fetchVillage(userId: number) {
  return prisma.village.findUnique({
    where: { userId },
    include: {
      user: true,
      tiles: { include: { object: true } },
      objects: { include: { tile: true } },
    },
  });
}

/* =========================
   DAY-BASED LOGIC
========================= */

async function processFinishedClearing(
  tx: Prisma.TransactionClient,
  village: FullVillage,
  today: string
): Promise<VillageTile[]> {
  const finishedTiles = village.tiles.filter(
    t =>
      t.terrainState === 'CLEARING' &&
      isBeforeDay(t.clearingStartedDay, today)
  );

  if (!finishedTiles.length) return [];

  const baseReward = REWARDS.VILLAGE.CLEARING;
  const totalBaseReward = {
    coins: baseReward.coins * finishedTiles.length,
    exp: baseReward.exp * finishedTiles.length,
  };

  // Ensure dailyStreak is at least 1 for multiplier calculation if it's 0 or null
  const currentDailyStreak = village.user.dailyStreak && village.user.dailyStreak > 0 ? village.user.dailyStreak : 1;
  const finalReward = applyStreakMultiplier(totalBaseReward, currentDailyStreak);

  await tx.user.update({
    where: { id: village.user.id },
    data: {
      coins: { increment: finalReward.coins },
      exp: { increment: finalReward.exp },
    },
  });

  await tx.villageTile.updateMany({
    where: { id: { in: finishedTiles.map(t => t.id) } },
    data: {
      terrainType: 'EMPTY',
      terrainState: 'IDLE',
      clearingStartedDay: null,
    },
  });

  const streakMultiplier = village.user.dailyStreak && village.user.dailyStreak > 1 ? village.user.dailyStreak : 0;
  let streakBonusText = '';
  if (streakMultiplier > 1) {
    streakBonusText = ` Ваша серия визитов (${streakMultiplier}) увеличила награду.`;
  }
  
  const events = finishedTiles.map(t => {
    // Apply streak multiplier with a default of 1 if streak is not active
    const tileReward = applyStreakMultiplier(baseReward, currentDailyStreak);
    return {
      villageId: village.id,
      type: t.terrainType === 'BLOCKED_TREE' ? 'CLEAR_TREE' : 'CLEAR_STONE',
      message: `Участок (${t.x}, ${t.y}) расчищен.` + streakBonusText,
      tileX: t.x,
      tileY: t.y,
      coins: tileReward.coins,
      exp: tileReward.exp,
    };
  });

  await tx.villageEvent.createMany({
    data: events,
  });

  return finishedTiles;
}

async function processFieldExp(
  tx: Prisma.TransactionClient,
  village: FullVillage,
  today: string
): Promise<number> {
  const fieldsNeedingUpdate = village.objects.filter(
    (o) => o.type === 'FIELD' && isBeforeDay(o.lastExpDay, today)
  );

  if (!fieldsNeedingUpdate.length) return 0;

  const wells = village.objects.filter(o => o.type === 'WELL');
  let totalBaseExpGained = 0;
  const eventsToCreate: any[] = [];
  
  const currentDailyStreak = village.user.dailyStreak && village.user.dailyStreak > 0 ? village.user.dailyStreak : 1;
  let streakBonusText = '';
  if (currentDailyStreak > 1) {
    streakBonusText = ` Ваша серия визитов (${currentDailyStreak}) увеличила награду.`;
  }

  for (const field of fieldsNeedingUpdate) {
    const daysMissed = field.lastExpDay ? daysSince(field.lastExpDay, today) : 1;
    let expGainedForField = 0;
    let wellBonusText = '';

    for (let i = 0; i < daysMissed; i++) {
      let dailyFieldExp = REWARDS.VILLAGE.FIELD_EXP.BASE;

      const isNearWell = wells.some(well => 
        Math.abs(well.tile.x - field.tile.x) <= 1 && 
        Math.abs(well.tile.y - field.tile.y) <= 1 &&
        (well.tile.x !== field.tile.x || well.tile.y !== field.tile.y)
      );

      if (isNearWell) {
        dailyFieldExp *= REWARDS.VILLAGE.FIELD_EXP.WELL_MULTIPLIER;
        wellBonusText = ' Рядом с колодцем урожай удвоился!';
      }
      expGainedForField += dailyFieldExp;

      // Create an event for each day the field gained experience
      eventsToCreate.push({
        villageId: village.id,
        type: 'FIELD_EXP',
        message: `Поле (${field.tile.x}, ${field.tile.y}) принесло опыт.` + wellBonusText + streakBonusText,
        tileX: field.tile.x,
        tileY: field.tile.y,
        coins: 0,
        exp: applyStreakMultiplier({ coins: 0, exp: dailyFieldExp }, currentDailyStreak).exp,
      });
    }
    totalBaseExpGained += expGainedForField; // This is base exp without final streak multiplier for total
  }

  const finalTotalExp = applyStreakMultiplier({ coins: 0, exp: totalBaseExpGained }, currentDailyStreak);

  if (finalTotalExp.exp > 0) { // Check final total exp after multiplier
    await tx.user.update({
      where: { id: village.user.id },
      data: { exp: { increment: finalTotalExp.exp } },
    });
  }

  await tx.villageObject.updateMany({
    where: { id: { in: fieldsNeedingUpdate.map((f) => f.id) } },
    data: { lastExpDay: today },
  });

  if (eventsToCreate.length > 0) {
    await tx.villageEvent.createMany({
      data: eventsToCreate,
    });
  }
  
  return finalTotalExp.exp; // Return the final experience gained
}

async function autoStartClearing(
  tx: Prisma.TransactionClient,
  village: FullVillage,
  today: string,
  justFinishedTiles: VillageTile[] = []
): Promise<number> {
  // Count total capacity for clearing
  const lumberjackCapacity = village.objects.filter(o => o.type === 'LUMBERJACK').length;
  const quarryCapacity = village.objects.filter(o => o.type === 'QUARRY').length;

  // We must account for the tiles that were *just* finished in this transaction.
  const finishedTreeTiles = justFinishedTiles.filter(t => t.terrainType === 'BLOCKED_TREE').length;
  const finishedStoneTiles = justFinishedTiles.filter(t => t.terrainType === 'BLOCKED_STONE').length;

  const busyTreesInSnapshot = village.tiles.filter(
    t => t.terrainType === 'BLOCKED_TREE' && t.terrainState === 'CLEARING'
  ).length;
  const busyStonesInSnapshot = village.tiles.filter(
    t => t.terrainType === 'BLOCKED_STONE' && t.terrainState === 'CLEARING'
  ).length;

  // Correctly calculate busy workers by subtracting those that just finished
  const currentBusyTrees = Math.max(0, busyTreesInSnapshot - finishedTreeTiles);
  const currentBusyStones = Math.max(0, busyStonesInSnapshot - finishedStoneTiles);

  const freeLumberjacks = Math.max(0, lumberjackCapacity - currentBusyTrees);
  const freeQuarries = Math.max(0, quarryCapacity - currentBusyStones);

  // Find idle tiles that are not among those just finished (though they should be EMPTY now anyway)
  const idleTrees = village.tiles.filter(t => t.terrainType === 'BLOCKED_TREE' && t.terrainState === 'IDLE');
  const idleStones = village.tiles.filter(t => t.terrainType === 'BLOCKED_STONE' && t.terrainState === 'IDLE');
  
  const treesToStart = idleTrees.slice(0, freeLumberjacks);
  const stonesToStart = idleStones.slice(0, freeQuarries);

  const tilesToStart = [...treesToStart, ...stonesToStart];

  if (!tilesToStart.length) {
    return 0;
  }

  await tx.villageTile.updateMany({
    where: { id: { in: tilesToStart.map(t => t.id) } },
    data: {
      terrainState: 'CLEARING',
      clearingStartedDay: today,
    },
  });

  return tilesToStart.length;
}
