import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import { OnEvent } from '@nestjs/event-emitter';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Quest } from './quest.entity';
import { QuestArc } from './quest-arc.entity';
import { PlayerQuest } from './player-quest.entity';
import { PlayerQuestArc } from './player-quest-arc.entity';
import { Character } from '../character/entities/character.entity';
import { getUnlockedZones } from '../common/zone-access';

const MAX_ACTIVE_QUESTS = 3;

@Injectable()
export class QuestService {
  constructor(
    @InjectRepository(Quest)
    private readonly questRepo: Repository<Quest>,
    @InjectRepository(QuestArc)
    private readonly arcRepo: Repository<QuestArc>,
    @InjectRepository(PlayerQuest)
    private readonly playerQuestRepo: Repository<PlayerQuest>,
    @InjectRepository(PlayerQuestArc)
    private readonly playerArcRepo: Repository<PlayerQuestArc>,
    @InjectRepository(Character)
    private readonly characterRepo: Repository<Character>,
    private readonly dataSource: DataSource,
    private readonly eventEmitter: EventEmitter2,
  ) {}

  async getAvailable(characterId: string) {
    const character = await this.characterRepo.findOne({ where: { id: characterId } });
    if (!character) throw new BadRequestException('Aucun personnage');

    // All player quests (any status)
    const playerQuests = await this.playerQuestRepo.find({
      where: { characterId },
      select: ['questId', 'status'],
    });
    const questStatusMap = new Map(playerQuests.map((pq) => [pq.questId, pq.status]));

    // Zone locking — only show quests from unlocked zones
    const unlockedZones = await getUnlockedZones(characterId, this.arcRepo, this.playerArcRepo);

    const quests = await this.questRepo.find({
      relations: ['arc'],
      order: { arcOrder: 'ASC' },
    });

    return quests.filter((q) => {
      if (q.minLevel > character.level) return false;
      // Arc quests managed from arc panel — not in available
      if (q.arcId) return false;

      const status = questStatusMap.get(q.id);
      if (status === 'active' || status === 'completed') return false;
      if (status === 'claimed' && !q.repeatable) return false;

      return true;
    });
  }

  async getActive(characterId: string) {
    return this.playerQuestRepo.find({
      where: { characterId, status: 'active' },
      relations: ['quest', 'quest.arc'],
      order: { acceptedAt: 'ASC' },
    });
  }

  async getCompleted(characterId: string) {
    return this.playerQuestRepo.find({
      where: [
        { characterId, status: 'completed' },
        { characterId, status: 'claimed' },
      ],
      relations: ['quest', 'quest.arc'],
      order: { completedAt: 'DESC' },
      take: 20,
    });
  }

  async accept(questId: string, characterId: string) {
    const quest = await this.questRepo.findOne({ where: { id: questId } });
    if (!quest) throw new NotFoundException('Quête introuvable');

    const character = await this.characterRepo.findOne({ where: { id: characterId } });
    if (!character) throw new BadRequestException('Aucun personnage');

    if (quest.minLevel > character.level) {
      throw new BadRequestException(`Niveau ${quest.minLevel} requis`);
    }

    // Check active quest count (repeatable quests don't count toward the limit)
    if (!quest.repeatable) {
      const activeNonRepeatable = await this.playerQuestRepo
        .createQueryBuilder('pq')
        .innerJoin('pq.quest', 'q')
        .where('pq.character_id = :characterId', { characterId })
        .andWhere('pq.status = :status', { status: 'active' })
        .andWhere('q.repeatable = false')
        .getCount();
      if (activeNonRepeatable >= MAX_ACTIVE_QUESTS) {
        throw new BadRequestException(`Maximum ${MAX_ACTIVE_QUESTS} quêtes actives (hors répétables)`);
      }
    }

    // Check not already active
    const existing = await this.playerQuestRepo.findOne({
      where: { characterId, questId },
    });
    if (existing && existing.status === 'active') {
      throw new BadRequestException('Quête déjà acceptée');
    }
    if (existing && existing.status === 'claimed' && !quest.repeatable) {
      throw new BadRequestException('Quête déjà complétée');
    }

    // If repeatable and already claimed, reset
    if (existing && quest.repeatable) {
      existing.progress = 0;
      existing.status = 'active';
      existing.completedAt = null;
      return this.playerQuestRepo.save(existing);
    }

    const pq = this.playerQuestRepo.create({
      characterId,
      questId,
      progress: 0,
      status: 'active',
    });
    return this.playerQuestRepo.save(pq);
  }

  async claim(playerQuestId: string, characterId: string) {
    return this.dataSource.transaction(async (manager) => {
      const pq = await manager.getRepository(PlayerQuest).findOne({
        where: { id: playerQuestId, characterId },
        relations: ['quest', 'quest.arc'],
      });
      if (!pq) throw new NotFoundException('Quête introuvable');
      if (pq.status !== 'completed') throw new BadRequestException('Quête pas encore terminée');

      pq.status = 'claimed';
      await manager.save(pq);

      // Credit rewards
      const character = await manager
        .getRepository(Character)
        .createQueryBuilder('c')
        .setLock('pessimistic_write')
        .where('c.id = :id', { id: characterId })
        .getOne();

      if (character) {
        character.xp += pq.quest.rewardXp;
        character.gold += pq.quest.rewardGold;
        character.totalGoldEarned = Number(character.totalGoldEarned ?? 0) + pq.quest.rewardGold;

        // Apply level up
        const { applyXpGain } = require('../combat/combat.engine');
        const levelUp = applyXpGain(character.level, character.xp, 0);
        // applyXpGain with 0 earned recalculates from existing XP
        // Actually we need to apply the XP gain properly
        const result = applyXpGain(character.level, character.xp - pq.quest.rewardXp, pq.quest.rewardXp);
        character.level = result.newLevel;
        character.xp = result.newXp;
        character.statPoints = (character.statPoints ?? 0) + result.statPointsGained;

        if (result.statPointsGained > 0) {
          character.hpMax += 0; // no auto HP increase from quests
        }

        await manager.save(character);
      }

      // Emit achievement events
      this.eventEmitter.emit('achievement.check', {
        characterId, type: 'quests_completed', increment: 1,
      });

      // Check arc completion
      if (pq.quest.arcId) {
        await this.checkArcCompletion(characterId, pq.quest.arcId, manager);
      }

      return {
        claimed: true,
        quest: pq.quest.name,
        rewardXp: pq.quest.rewardXp,
        rewardGold: pq.quest.rewardGold,
        rewardTitle: pq.quest.rewardTitle,
        levelUp: character ? { level: character.level, statPoints: character.statPoints } : null,
      };
    });
  }

  private async checkArcCompletion(characterId: string, arcId: string, manager: any) {
    // Get all quests in this arc
    const arcQuests = await manager.getRepository(Quest).find({
      where: { arcId },
    });

    // Check all are claimed
    const claimed = await manager.getRepository(PlayerQuest).find({
      where: arcQuests.map((q) => ({ characterId, questId: q.id, status: 'claimed' as const })),
    });

    if (claimed.length >= arcQuests.length) {
      // Arc complete!
      let playerArc = await manager.getRepository(PlayerQuestArc).findOne({
        where: { characterId, questArcId: arcId },
      });

      if (!playerArc) {
        playerArc = manager.getRepository(PlayerQuestArc).create({
          characterId,
          questArcId: arcId,
          completed: true,
          completedAt: new Date(),
        });
      } else if (!playerArc.completed) {
        playerArc.completed = true;
        playerArc.completedAt = new Date();
      } else {
        return; // already completed
      }

      await manager.save(playerArc);

      this.eventEmitter.emit('achievement.check', {
        characterId, type: 'quest_arc_completed', increment: 1,
      });
    }
  }

  async abandon(playerQuestId: string, characterId: string) {
    const pq = await this.playerQuestRepo.findOne({
      where: { id: playerQuestId, characterId },
      relations: ['quest'],
    });
    if (!pq) throw new NotFoundException('Quête introuvable');
    if (pq.status !== 'active') throw new BadRequestException('Seules les quêtes actives peuvent être abandonnées');

    await this.playerQuestRepo.remove(pq);
    return { abandoned: true, quest: pq.quest.name };
  }

  // --- Event listeners for quest progress ---

  @OnEvent('quest.progress')
  async handleQuestProgress(event: {
    characterId: string;
    type: string;
    targetId?: string;
    increment: number;
  }) {
    const { characterId, type, targetId, increment } = event;

    // Find active quests matching this event
    const activeQuests = await this.playerQuestRepo.find({
      where: { characterId, status: 'active' },
      relations: ['quest'],
    });

    for (const pq of activeQuests) {
      const q = pq.quest;
      if (q.objectiveType !== type) continue;

      // For targeted objectives, check target matches
      if (q.objectiveTargetId && q.objectiveTargetId !== targetId) continue;

      pq.progress = Math.min(pq.progress + increment, q.objectiveCount);

      if (pq.progress >= q.objectiveCount) {
        pq.status = 'completed';
        pq.completedAt = new Date();
      }

      await this.playerQuestRepo.save(pq);
    }
  }

  // --- Arcs ---

  async getArcs(characterId: string) {
    const character = await this.characterRepo.findOne({ where: { id: characterId } });
    const playerLevel = character?.level ?? 1;

    const unlockedZones = await getUnlockedZones(characterId, this.arcRepo, this.playerArcRepo);

    const arcs = await this.arcRepo.find({
      relations: ['quests'],
      order: { sortOrder: 'ASC' },
    });

    const playerArcs = await this.playerArcRepo.find({ where: { characterId } });
    const arcMap = new Map(playerArcs.map((pa) => [pa.questArcId, pa]));

    const playerQuests = await this.playerQuestRepo.find({
      where: { characterId },
      select: ['questId', 'status', 'id', 'progress'],
    });
    const questDataMap = new Map(playerQuests.map((pq) => [pq.questId, pq]));

    return arcs.map((arc) => {
      const zoneUnlocked = !arc.zone || unlockedZones.includes(arc.zone);
      return {
        ...arc,
        zoneUnlocked,
        completed: arcMap.get(arc.id)?.completed ?? false,
        completedAt: arcMap.get(arc.id)?.completedAt ?? null,
        quests: arc.quests
          .sort((a, b) => a.arcOrder - b.arcOrder)
          .map((q) => {
            const pq = questDataMap.get(q.id);
            return {
              ...q,
              playerStatus: pq?.status ?? 'available',
              playerQuestId: pq?.id ?? null,
              progress: pq?.progress ?? 0,
              canAccept: zoneUnlocked && !pq && q.minLevel <= playerLevel,
              levelOk: q.minLevel <= playerLevel,
            };
          }),
        progress: {
          completed: arc.quests.filter((q) => questDataMap.get(q.id)?.status === 'claimed').length,
          total: arc.quests.length,
        },
      };
    });
  }
}
