import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { LoggingService } from 'src/database/logging/logging.service';
import { Player } from 'src/players/entities/player.entity';
import { TeamRole } from 'src/team-roles/entities/team-roles.entity';
import { CreateTeamSettingDTO } from 'src/team-settings/dto/create-team-setting.dto';
import { TeamSetting } from 'src/team-settings/entities/team-setting.entity';
import { TEAM_SETTING_DEFAULTS } from 'src/team-settings/team-setting-defaults';
import { TeamWalletTransaction } from 'src/team-wallet-transactions/entities/team-wallet-transaction.entity';
import { Transaction } from 'src/transactions/entitites/transaction.entity';
import { TeamRolesEnum } from 'src/team-roles/team-roles.enum';
import { User } from 'src/users/entities/user.entity';
import { DataSource, EntityManager, Repository } from 'typeorm';
import {
  TransactionsQueryDto,
  TransactionsSortableField,
} from 'src/transactions/dto/transactions-query.dto';
import { CreateTeamDTO } from './dto/create-team.dto';
import { UpdatePlayerProfileDto } from './dto/update-player-profile.dto';
import { Team } from './entities/team.entity';
import { TeamAccessService } from './team-access.service';
import { DEACTIVATION_ADJUSTMENT_NOTE_PREFIX } from './team-members.service';

export interface TeamActivityRow {
  id: number;
  date: string;
  amount: number;
  type: string;
  note: string | null;
  playerName?: string;
  isTeamWalletTransaction: boolean;
}

@Injectable()
export class TeamsService {
  constructor(
    @InjectRepository(Team)
    private repository: Repository<Team>,
    @InjectRepository(Player)
    private playerRepository: Repository<Player>,
    @InjectRepository(Transaction)
    private transactionsRepository: Repository<Transaction>,
    @InjectRepository(TeamRole)
    private rolesRepository: Repository<TeamRole>,
    @InjectRepository(TeamSetting)
    private settingsRepository: Repository<TeamSetting>,
    @InjectRepository(TeamWalletTransaction)
    private teamWalletTransactionRepository: Repository<TeamWalletTransaction>,
    private logger: LoggingService,
    private access: TeamAccessService,
    @InjectRepository(User)
    private usersRepository: Repository<User>,
    private dataSource: DataSource,
  ) {}

  async getOverview(teamId: string, actorUserId: string) {
    const id = Number(teamId);

    await this.access.assertMember(Number(actorUserId), id);

    const team = await this.repository.findOneOrFail({
      where: { id },
      relations: ['players'],
    });

    if (team.players) {
      const out = team.players
        .filter((p) => p.active)
        .reduce((acc, succ) => acc + Number(succ.balance), 0);
      team.outstanding = out * -1;
    } else {
      team.outstanding = 0;
    }
    team.balance = Number(team.balance);

    return team;
  }

  async getOverviewByAlias(alias: string) {
    const team = await this.repository.findOneOrFail({
      where: { alias },
      relations: ['players'],
    });

    if (team.players) {
      const out = team.players
        .filter((p) => p.active)
        .reduce((acc, succ) => acc + Number(succ.balance), 0);
      team.outstanding = out * -1;
    } else {
      team.outstanding = 0;
    }
    return team;
  }

  async getUserTransactionsFromTeam(teamId: string, userId: string) {
    const player = await this.playerRepository.findOne({
      where: { id: Number(userId) },
      relations: ['transactions'],
    });

    if (!player) {
      throw new HttpException(
        {
          status: HttpStatus.NOT_FOUND,
          errors: {
            user: 'user_not_found',
          },
        },
        HttpStatus.NOT_FOUND,
      );
    }
    const transactions = await this.transactionsRepository.find({
      where: {
        player: {
          id: player.id,
        },
      },
      order: {
        date: 'DESC',
      },
      take: 20,
    });
    return transactions;
  }

  async createNewPlayer(
    id: string,
    player: { firstName: string; lastName: string; teamRole: any },
    actorUserId: string,
  ) {
    await this.access.assertManager(Number(actorUserId), Number(id));

    if (typeof player.teamRole == 'string') {
      player.teamRole = await this.rolesRepository.findOneBy({
        name: player.teamRole,
      });
    } else if (typeof player.teamRole == 'number') {
      player.teamRole = await this.rolesRepository.findOneBy({
        id: player.teamRole,
      });
    } else if (!player.teamRole) {
      player.teamRole = await this.rolesRepository.findOneBy({
        id: 1, // player
      });
    }
    const team = await this.repository.findOneBy({
      id: Number(id),
    });

    if (!team) {
      throw new HttpException('No Team Found', HttpStatus.UNPROCESSABLE_ENTITY);
    }
    const p = {
      ...player,
      team,
    };

    const created: Player = this.playerRepository.create(p);

    const playerSaved: Player = await this.playerRepository.save(created);

    await this.logger.info({
      event: 'player_creation',
      details: `Spieler ${playerSaved.id}, ${p.firstName} ${p.lastName} erstellt`,
      userId: Number(id),
    });
    return playerSaved;
  }

  async createNewTeam(teamdto: CreateTeamDTO, userId: string) {
    const team = await this.dataSource.transaction(async (manager) => {
      const teamRepository = manager.getRepository(Team);
      const createTeam = teamRepository.create(teamdto);
      const savedTeam = await teamRepository.save(createTeam);

      await this.generateBasicTeamSettings(manager, savedTeam);
      await this.addCreatorAsCaptain(manager, savedTeam, userId);

      return savedTeam;
    });

    await this.logger.info({
      event: 'team_create',
      details: `created team id: ${team.id}`,
      userId: Number(userId),
    });
    return team;
  }

  private async addCreatorAsCaptain(
    manager: EntityManager,
    team: Team,
    userId: string,
  ): Promise<void> {
    const [user, captainRole] = await Promise.all([
      manager.getRepository(User).findOneBy({ id: Number(userId) }),
      manager.getRepository(TeamRole).findOneBy({ id: TeamRolesEnum.captain }),
    ]);

    const playerRepository = manager.getRepository(Player);
    const player = playerRepository.create({
      firstName: user?.firstName ?? '',
      lastName: user?.lastName ?? '',
      team,
      teamRole: captainRole,
      user: user ?? undefined,
    });

    await playerRepository.save(player);
  }

  private async generateBasicTeamSettings(
    manager: EntityManager,
    team: Team,
  ): Promise<void> {
    const settingsRepository = manager.getRepository(TeamSetting);
    const settings: CreateTeamSettingDTO[] = Object.entries(
      TEAM_SETTING_DEFAULTS,
    ).map(([key, value]) => ({
      key: key as CreateTeamSettingDTO['key'],
      value: String(value),
      team,
    }));

    await settingsRepository.save(
      settings.map((s) => settingsRepository.create(s)),
    );
  }

  async getTeamTransactions(teamId: string | number, userId: string | number) {
    const start = new Date();
    teamId = Number(teamId);

    await this.access.assertMember(Number(userId), teamId);

    const team = await this.repository.findOneOrFail({
      where: {
        id: teamId,
      },
      relations: ['players', 'players.transactions', 'transactions'],
    });

    const transactions = [];

    for (const t of team.transactions) {
      transactions.push({
        id: t.id,
        date: t.date,
        amount: t.amount,
        type: t.type.name,
        note: t.note,
        isTeamWalletTransaction: true,
      });
    }

    for (const p of team.players) {
      for (const t of p.transactions) {
        transactions.push({
          id: t.id,
          date: t.date,
          amount: t.amount,
          type: t.type.name,
          note: t.note,
          playerName: p.firstName + ' ' + p.lastName,
          isTeamWalletTransaction: false,
        });
      }
    }

    const result = transactions.sort((a, b) => (a.date < b.date ? 1 : -1));
    const duration = new Date().getTime() - start.getTime();

    await this.logger.debug({
      event: 'team_transaction_get',
      details: `load ${result.length} transactions for TeamID: ${team.id}`,
      userId: Number(userId),
      duration,
    });
    return result;
  }

  async getTeamTransactionsJournal(
    teamId: string | number,
    userId: string | number,
    query: TransactionsQueryDto,
  ): Promise<{ data: TeamActivityRow[]; total: number }> {
    const id = Number(teamId);

    await this.access.assertMember(Number(userId), id);

    const team = await this.repository.findOneOrFail({
      where: { id },
      relations: ['players', 'players.transactions', 'transactions'],
    });

    const transactions: TeamActivityRow[] = [];

    for (const t of team.transactions) {
      transactions.push({
        id: t.id,
        date: t.date,
        amount: Number(t.amount),
        type: t.type.name,
        note: t.note,
        isTeamWalletTransaction: true,
      });
    }

    for (const p of team.players) {
      for (const t of p.transactions) {
        transactions.push({
          id: t.id,
          date: t.date,
          amount: Number(t.amount),
          type: t.type.name,
          note: t.note,
          playerName: p.firstName + ' ' + p.lastName,
          isTeamWalletTransaction: false,
        });
      }
    }

    const search = query.search?.trim().toLowerCase();
    const filtered = transactions.filter((row) => {
      if (query.type && row.type !== query.type) return false;
      if (search) {
        const haystack = `${row.playerName ?? 'Teamkasse'} ${row.note ?? ''}`.toLowerCase();
        if (!haystack.includes(search)) return false;
      }
      return true;
    });

    const sortBy = query.sortBy ?? 'date';
    const direction = query.sortDir === 'asc' ? 1 : -1;
    const sorted = [...filtered].sort((a, b) => {
      const aValue = this.transactionSortValue(a, sortBy);
      const bValue = this.transactionSortValue(b, sortBy);
      if (aValue < bValue) return -1 * direction;
      if (aValue > bValue) return 1 * direction;
      return 0;
    });

    const page = query.page ?? 1;
    const limit = query.limit ?? 25;
    const start = (page - 1) * limit;
    const data = sorted.slice(start, start + limit);

    return { data, total: filtered.length };
  }

  private transactionSortValue(
    row: TeamActivityRow,
    field: TransactionsSortableField,
  ): string | number {
    switch (field) {
      case 'amount':
        return row.amount;
      case 'playerName':
        return (row.playerName ?? 'Teamkasse').toLowerCase();
      case 'type':
        return row.type;
      case 'date':
      default:
        return row.date;
    }
  }

  async getOverviewStats(
    teamId: string | number,
    actorUserId: string | number,
  ): Promise<{
    balanceHistory: { month: string; balance: number; theoreticalBalance: number }[];
    monthlyFlow: { month: string; income: number; expense: number; penalties: number }[];
    topOutstanding: { playerId: number; playerName: string; balance: number }[];
  }> {
    const id = Number(teamId);

    // Read access: any active team member may view the KPI charts (same
    // audience as the existing :id/overview page), not just managers.
    await this.access.assertMember(Number(actorUserId), id);

    const team = await this.repository.findOneOrFail({
      where: { id },
      relations: ['players', 'players.transactions', 'transactions'],
    });

    const players = team.players ?? [];
    const teamTransactions = team.transactions ?? [];

    // Only payment/credit/expense movements represent actual cash flow and
    // are the only types that touch team.balance (see setBalance() on
    // TeamWalletTransaction/Transaction) — fine/levy/fee raise a player's
    // debt but never move money, so they are excluded entirely.
    const movements: { date: string; amount: number; type: string }[] = [];
    // Fine/levy/fee (type.id > 10) raise a player's debt but never move real
    // cash, so they're kept out of `movements` and tracked separately here
    // purely to chart "how much was booked as penalties/levies this month".
    const penaltyMovements: { date: string; amount: number }[] = [];

    for (const t of teamTransactions) {
      movements.push({ date: t.date, amount: Number(t.amount), type: t.type.name });
    }

    for (const p of players) {
      for (const t of p.transactions ?? []) {
        if (t.type.name === 'payment') {
          movements.push({ date: t.date, amount: Number(t.amount), type: t.type.name });
        } else if (t.type.id > 10) {
          penaltyMovements.push({ date: t.date, amount: Number(t.amount) });
        }
      }
    }

    movements.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));

    const topOutstanding = players
      .filter((p) => p.active && Number(p.balance) < 0)
      .sort((a, b) => Number(a.balance) - Number(b.balance))
      .slice(0, 10)
      .map((p) => ({
        playerId: p.id,
        playerName: p.firstName + ' ' + p.lastName,
        balance: this.round(Math.abs(Number(p.balance))),
      }));

    // A brand-new team with no relevant movements at all (ever, not just in
    // the last 12 months) has nothing to chart — return empty arrays so the
    // frontend's empty-state fires instead of rendering 12 flat zero points.
    if (movements.length === 0) {
      return { balanceHistory: [], monthlyFlow: [], topOutstanding };
    }

    const months = this.getLast12Months();

    // team.balance is the one authoritative, current value — it can include
    // historical adjustments (e.g. from the removed legacy backend) that
    // don't trace back to the visible payment/credit/expense rows. Forward-
    // summing the movements from zero would silently drift away from
    // team.balance for such teams. Instead we anchor to team.balance and
    // walk the movements backward (newest first), "undoing" each one to
    // reconstruct earlier month-end balances — this guarantees the most
    // recent point always equals team.balance by construction, regardless
    // of undocumented history.
    const signedMovements = movements.map((m) => ({
      date: m.date,
      amount: this.signedFlowAmount(m),
    }));
    const currentBalance = Number(team.balance);
    const rawBalances = this.reconstructBackward(months, currentBalance, signedMovements);
    const balanceHistory = months.map((month, index) => ({
      month,
      balance: this.round(rawBalances[index]),
    }));

    const monthlyFlow = months.map((month) => {
      const monthMovements = movements.filter((m) => m.date.slice(0, 7) === month);
      const income = monthMovements
        .filter((m) => m.type === 'payment' || m.type === 'credit')
        .reduce((sum, m) => sum + m.amount, 0);
      const expense = monthMovements
        .filter((m) => m.type === 'expense')
        .reduce((sum, m) => sum + m.amount, 0);
      const penalties = penaltyMovements
        .filter((m) => m.date.slice(0, 7) === month)
        .reduce((sum, m) => sum + m.amount, 0);
      return {
        month,
        income: this.round(income),
        expense: this.round(expense),
        penalties: this.round(penalties),
      };
    });

    const outstandingHistory = this.reconstructOutstandingHistory(months, players);
    const balanceHistoryWithTheoretical = balanceHistory.map((point, index) => ({
      ...point,
      theoreticalBalance: this.round(point.balance + outstandingHistory[index]),
    }));

    return { balanceHistory: balanceHistoryWithTheoretical, monthlyFlow, topOutstanding };
  }

  private getLast12Months(): string[] {
    const now = new Date();
    const months: string[] = [];
    for (let i = 11; i >= 0; i--) {
      const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
      months.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`);
    }
    return months;
  }

  private signedFlowAmount(movement: { amount: number; type: string }): number {
    return movement.type === 'expense' ? -movement.amount : movement.amount;
  }

  private reconstructBackward(
    months: string[],
    currentValue: number,
    signedMovements: { date: string; amount: number }[],
  ): number[] {
    const descending = [...signedMovements].sort((a, b) =>
      a.date > b.date ? -1 : a.date < b.date ? 1 : 0,
    );
    let futureSum = 0;
    let index = 0;
    return [...months]
      .reverse()
      .map((month) => {
        while (index < descending.length && descending[index].date.slice(0, 7) > month) {
          futureSum += descending[index].amount;
          index++;
        }
        return currentValue - futureSum;
      })
      .reverse();
  }

  private reconstructOutstandingHistory(months: string[], players: Player[]): number[] {
    const activePlayers = players.filter((p) => p.active);
    const totals = months.map(() => 0);

    for (const player of activePlayers) {
      const realTransactions = (player.transactions ?? []).filter(
        (t) => !t.note?.startsWith(DEACTIVATION_ADJUSTMENT_NOTE_PREFIX),
      );
      const playerHistory = this.reconstructPlayerBalanceHistory(
        months,
        Number(player.balance),
        realTransactions,
      );
      playerHistory.forEach((balance, index) => {
        totals[index] += balance;
      });
    }

    // House convention (see getOverview()'s team.outstanding = out * -1): "outstanding"
    // is a positive number when players owe money, negative when they're in credit.
    return totals.map((total) => total * -1);
  }

  private reconstructPlayerBalanceHistory(
    months: string[],
    currentBalance: number,
    transactions: Transaction[],
  ): number[] {
    const signedMovements = transactions.map((t) => {
      const rawAmount = Number(t.amount);
      const amount = t.type && t.type.id > 10 && rawAmount > 0 ? -rawAmount : rawAmount;
      return { date: t.date, amount };
    });

    return this.reconstructBackward(months, currentBalance, signedMovements);
  }

  private round(value: number): number {
    return Math.round(value * 100) / 100;
  }

  async updatePlayer(
    teamId: string,
    playerDTO: UpdatePlayerProfileDto,
    actorUserId: string,
  ) {
    const player = await this.playerRepository.findOneOrFail({
      where: {
        id: playerDTO.id,
      },
      relations: ['team'],
    });

    if (player.team?.id !== Number(teamId)) {
      throw new HttpException('No Player Found', HttpStatus.NOT_FOUND);
    }

    await this.access.assertManager(Number(actorUserId), Number(teamId));

    player.firstName = playerDTO.firstName;
    player.lastName = playerDTO.lastName;

    return this.playerRepository.save(player);
  }
}
