import { Op } from 'sequelize';
import FriendlyMatchShared from '../models/FriendlyMatchShared.js';
import FriendlyMatchInvitation from '../models/FriendlyMatchInvitation.js';
import UserClub from '../models/UserClub.js';
import User from '../models/User.js';
import Club from '../models/Club.js';
import Member from '../models/Member.js';
import HttpError from '../exceptions/HttpError.js';
import { checkAccess, getUserByToken } from '../utils/userUtils.js';
import { sendFriendlyMatchInvitationEmail } from './emailService.js';

function cleanString(value, fallback = '') {
  const text = String(value ?? '').trim();
  return text || fallback;
}

function cleanOptionalString(value) {
  const text = String(value ?? '').trim();
  return text || null;
}

function normalizeArrayValue(value) {
  if (Array.isArray(value)) return value;
  if (typeof value === 'string') {
    try {
      const parsed = JSON.parse(value);
      return Array.isArray(parsed) ? parsed : [];
    } catch (error) {
      return [];
    }
  }
  return [];
}

function normalizeIdList(list) {
  if (typeof list === 'string') {
    try {
      list = JSON.parse(list);
    } catch (error) {
      return null;
    }
  }
  if (!Array.isArray(list)) return null;
  const seen = new Set();
  const result = [];
  for (const value of list) {
    const id = Number.parseInt(value, 10);
    if (!Number.isInteger(id) || seen.has(id)) continue;
    seen.add(id);
    result.push(id);
  }
  return result;
}

function memberIdsFromParticipants(...participantLists) {
  const seen = new Set();
  const result = [];
  for (const list of participantLists) {
    for (const participant of normalizeArrayValue(list)) {
      if (participant?.type !== 'member') continue;
      const id = Number.parseInt(participant.memberId, 10);
      if (!Number.isInteger(id) || seen.has(id)) continue;
      seen.add(id);
      result.push(id);
    }
  }
  return result;
}

function mergeIdLists(...lists) {
  const seen = new Set();
  const result = [];
  for (const list of lists) {
    const normalized = normalizeIdList(list) || [];
    for (const id of normalized) {
      if (seen.has(id)) continue;
      seen.add(id);
      result.push(id);
    }
  }
  return result;
}

function normalizeTextForSearch(value) {
  return String(value ?? '')
    .normalize('NFKD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(/[^a-zA-Z0-9\s]/g, ' ')
    .replace(/\s+/g, ' ')
    .trim()
    .toLowerCase();
}

function isClubInvolved(clubId, match) {
  const id = Number.parseInt(clubId, 10);
  return Number(match.homeClubId) === id || Number(match.guestClubId) === id;
}

function isSharedMatchLocked(match) {
  const dateText = cleanString(match?.date);
  if (!dateText) return false;
  const [year, month, day] = dateText.split('-').map((value) => Number.parseInt(value, 10));
  if (!year || !month || !day) return false;
  const endsAt = new Date(year, month - 1, day, 23, 59, 59, 999);
  return endsAt.getTime() <= Date.now();
}

function assertSharedMatchEditable(match) {
  if (isSharedMatchLocked(match)) {
    throw new HttpError('Der Termin ist verstrichen. Das Freundschaftsspiel ist nur noch sichtbar.', 409);
  }
}

function canShowOpponentMembers(match) {
  return String(match?.status || '') === 'active' && !isSharedMatchLocked(match);
}

function toSharedScheduleRow(match, viewerClubId = null) {
  const hideOpponent = viewerClubId != null && !canShowOpponentMembers(match);
  const viewerIsHome = Number(viewerClubId) === Number(match.homeClubId);
  const rawHomeParticipants = normalizeArrayValue(match.homeParticipants);
  const rawGuestParticipants = normalizeArrayValue(match.guestParticipants);
  const rawResultDetails = normalizeArrayValue(match.resultDetails);
  const homeParticipants = hideOpponent && !viewerIsHome ? [] : rawHomeParticipants;
  const guestParticipants = hideOpponent && viewerIsHome ? [] : rawGuestParticipants;
  const resultDetails = hideOpponent
    ? rawResultDetails.map((row) => ({
      ...row,
      homeName: viewerIsHome ? row?.homeName : '',
      guestName: viewerIsHome ? '' : row?.guestName,
    }))
    : rawResultDetails;

  return {
    id: match.id,
    friendlyMatchId: match.id,
    isFriendly: true,
    isSharedFriendly: true,
    date: match.date,
    time: match.startTime,
    homeClubId: match.homeClubId,
    guestClubId: match.guestClubId,
    homeTeam: { name: match.homeTeamName },
    guestTeam: { name: match.guestTeamName },
    location: {
      name: match.locationName || 'N/A',
      address: match.locationAddress || '',
      city: match.locationCity || '',
      zip: match.locationZip || '',
    },
    leagueDetails: { name: 'Freundschaftsspiel (Vereinsuebergreifend)' },
    homeMatchPoints: match.homeMatchPoints || 0,
    guestMatchPoints: match.guestMatchPoints || 0,
    isCompleted: match.isCompleted || false,
    matchSystem: match.matchSystem,
    singlesCount: match.singlesCount,
    doublesCount: match.doublesCount,
    winningSets: match.winningSets,
    homeParticipants,
    guestParticipants,
    resultDetails,
    playersReady: normalizeArrayValue(match.playersReady),
    playersPlanned: normalizeArrayValue(match.playersPlanned),
    playersPlayed: normalizeArrayValue(match.playersPlayed),
    status: match.status,
    isLocked: isSharedMatchLocked(match),
    matchName: match.matchName,
    createdFromInvitationId: match.createdFromInvitationId,
  };
}

function toInvitationDto(invitation) {
  return {
    id: invitation.id,
    fromClubId: invitation.fromClubId,
    toClubId: invitation.toClubId,
    proposedDate: invitation.proposedDate,
    proposedStartTime: invitation.proposedStartTime,
    proposedMatchName: invitation.proposedMatchName,
    message: invitation.message,
    status: invitation.status,
    createdByUserId: invitation.createdByUserId,
    acceptedByUserId: invitation.acceptedByUserId,
    acceptedAt: invitation.acceptedAt,
    createdAt: invitation.createdAt,
    updatedAt: invitation.updatedAt,
  };
}

class FriendlyMatchSharedService {
  async findByNameDateStartTime(userToken, clubId, query = {}) {
    await checkAccess(userToken, clubId);

    const nameNorm = normalizeTextForSearch(query.name);
    const date = cleanOptionalString(query.date);
    const startTime = cleanOptionalString(query.startTime);

    const where = {
      [Op.or]: [{ homeClubId: clubId }, { guestClubId: clubId }],
    };
    if (date) where.date = date;
    if (startTime) where.startTime = startTime;

    const matches = await FriendlyMatchShared.findAll({
      where,
      order: [['date', 'ASC'], ['startTime', 'ASC'], ['id', 'ASC']],
    });

    const out = matches
      .map((match) => {
        const row = toSharedScheduleRow(match, clubId);
        const combined = normalizeTextForSearch([
          row.matchName,
          row.homeTeam?.name,
          row.guestTeam?.name,
          `${row.homeTeam?.name || ''} ${row.guestTeam?.name || ''}`,
          `${row.guestTeam?.name || ''} ${row.homeTeam?.name || ''}`,
        ].join(' '));

        let confidence = 'medium';
        if (nameNorm && combined === nameNorm && date && startTime) confidence = 'exact';
        else if (nameNorm && combined.includes(nameNorm) && date && startTime) confidence = 'high';
        else if (nameNorm && !combined.includes(nameNorm)) return null;

        return {
          ...row,
          confidence,
          matchedBy: {
            name: Boolean(nameNorm),
            date: Boolean(date),
            startTime: Boolean(startTime),
          },
        };
      })
      .filter(Boolean);

    return out;
  }

  async listShared(userToken, clubId) {
    await checkAccess(userToken, clubId);
    const matches = await FriendlyMatchShared.findAll({
      where: {
        [Op.or]: [{ homeClubId: clubId }, { guestClubId: clubId }],
      },
      order: [['date', 'ASC'], ['startTime', 'ASC'], ['id', 'ASC']],
    });
    return matches.map((match) => toSharedScheduleRow(match, clubId));
  }

  async getSharedById(userToken, clubId, matchId) {
    await checkAccess(userToken, clubId);
    const match = await FriendlyMatchShared.findByPk(matchId);
    if (!match || !isClubInvolved(clubId, match)) {
      throw new HttpError('Gemeinsames Freundschaftsspiel nicht gefunden.', 404);
    }
    return toSharedScheduleRow(match, clubId);
  }

  async updateShared(userToken, clubId, matchId, payload = {}) {
    await checkAccess(userToken, clubId);

    const match = await FriendlyMatchShared.findByPk(matchId);
    if (!match || !isClubInvolved(clubId, match)) {
      throw new HttpError('Gemeinsames Freundschaftsspiel nicht gefunden.', 404);
    }
    assertSharedMatchEditable(match);

    const updates = {};
    if (Object.prototype.hasOwnProperty.call(payload, 'time') && !Object.prototype.hasOwnProperty.call(payload, 'startTime')) {
      payload.startTime = payload.time;
    }

    for (const field of ['date', 'startTime', 'matchName', 'homeTeamName', 'guestTeamName', 'locationName', 'locationAddress', 'locationCity', 'locationZip', 'matchSystem', 'status']) {
      if (Object.prototype.hasOwnProperty.call(payload, field)) {
        updates[field] = ['date', 'homeTeamName', 'guestTeamName', 'matchSystem', 'status'].includes(field)
          ? cleanString(payload[field])
          : cleanOptionalString(payload[field]);
      }
    }

    for (const field of ['singlesCount', 'doublesCount', 'winningSets', 'homeMatchPoints', 'guestMatchPoints']) {
      if (Object.prototype.hasOwnProperty.call(payload, field)) {
        updates[field] = Number.parseInt(payload[field], 10) || 0;
      }
    }

    if (Object.prototype.hasOwnProperty.call(payload, 'isCompleted')) {
      updates.isCompleted = Boolean(payload.isCompleted);
    }
    if (Object.prototype.hasOwnProperty.call(payload, 'homeParticipants')) {
      updates.homeParticipants = Array.isArray(payload.homeParticipants) ? payload.homeParticipants : [];
    }
    if (Object.prototype.hasOwnProperty.call(payload, 'guestParticipants')) {
      updates.guestParticipants = Array.isArray(payload.guestParticipants) ? payload.guestParticipants : [];
    }
    if (Object.prototype.hasOwnProperty.call(payload, 'resultDetails')) {
      updates.resultDetails = Array.isArray(payload.resultDetails) ? payload.resultDetails : [];
    }
    if (Object.prototype.hasOwnProperty.call(payload, 'resultDetails') || payload.isCompleted === true) {
      const playedParticipants = memberIdsFromParticipants(
        Object.prototype.hasOwnProperty.call(updates, 'homeParticipants') ? updates.homeParticipants : match.homeParticipants,
        Object.prototype.hasOwnProperty.call(updates, 'guestParticipants') ? updates.guestParticipants : match.guestParticipants,
      );
      updates.playersPlayed = mergeIdLists(match.playersPlayed, playedParticipants);
    }

    await match.update(updates);
    return toSharedScheduleRow(match, clubId);
  }

  async membersForSide(userToken, clubId, matchId, side) {
    await checkAccess(userToken, clubId);
    const match = await FriendlyMatchShared.findByPk(matchId);
    if (!match || !isClubInvolved(clubId, match)) {
      throw new HttpError('Gemeinsames Freundschaftsspiel nicht gefunden.', 404);
    }

    const requestedSide = side === 'guest' ? 'guest' : 'home';
    const targetClubId = requestedSide === 'guest' ? match.guestClubId : match.homeClubId;
    const isOpponent = Number(targetClubId) !== Number(clubId);
    if (isOpponent && !canShowOpponentMembers(match)) {
      return [];
    }

    return Member.findAll({
      where: { clubId: targetClubId, active: true },
      attributes: ['id', 'firstName', 'lastName', 'gender', 'clubId'],
      order: [['lastName', 'ASC'], ['firstName', 'ASC']],
    });
  }

  async updateSharedPlayers(userToken, clubId, matchId, payload = {}) {
    await checkAccess(userToken, clubId);

    const match = await FriendlyMatchShared.findByPk(matchId);
    if (!match || !isClubInvolved(clubId, match)) {
      throw new HttpError('Gemeinsames Freundschaftsspiel nicht gefunden.', 404);
    }
    assertSharedMatchEditable(match);

    const ready = normalizeIdList(payload.playersReady);
    const planned = normalizeIdList(payload.playersPlanned);
    const played = normalizeIdList(payload.playersPlayed);

    const currentClubMembers = await Member.findAll({
      where: { clubId },
      attributes: ['id'],
    });
    const currentClubMemberIds = new Set(currentClubMembers.map((member) => Number(member.id)));
    const mergeForCurrentClub = (existingValue, nextValue) => {
      if (nextValue == null) return normalizeArrayValue(existingValue);
      const existing = normalizeArrayValue(existingValue)
        .map((id) => Number.parseInt(id, 10))
        .filter((id) => Number.isInteger(id));
      const preservedOtherClub = existing.filter((id) => !currentClubMemberIds.has(id));
      const nextCurrentClub = nextValue.filter((id) => currentClubMemberIds.has(id));
      return [...new Set([...preservedOtherClub, ...nextCurrentClub])];
    };

    await match.update({
      playersReady: mergeForCurrentClub(match.playersReady, ready),
      playersPlanned: mergeForCurrentClub(match.playersPlanned, planned),
      playersPlayed: mergeForCurrentClub(match.playersPlayed, played),
    });

    return toSharedScheduleRow(match, clubId);
  }

  async removeShared(userToken, clubId, matchId) {
    await checkAccess(userToken, clubId);
    const match = await FriendlyMatchShared.findByPk(matchId);
    if (!match || !isClubInvolved(clubId, match)) {
      throw new HttpError('Gemeinsames Freundschaftsspiel nicht gefunden.', 404);
    }
    await match.destroy();
    return { success: true, id: Number(matchId) };
  }

  async createInvitation(userToken, fromClubId, payload = {}) {
    await checkAccess(userToken, fromClubId);

    const toClubId = Number.parseInt(payload.toClubId, 10);
    if (!Number.isInteger(toClubId)) {
      throw new HttpError('Zielverein fehlt oder ist ungueltig.', 400);
    }
    if (Number(toClubId) === Number(fromClubId)) {
      throw new HttpError('Ein Verein kann sich nicht selbst einladen.', 400);
    }

    const proposedDate = cleanString(payload.date);
    const proposedMatchName = cleanString(payload.matchName);
    if (!proposedDate || !proposedMatchName) {
      throw new HttpError('Datum und Matchname sind Pflichtfelder.', 400);
    }

    const user = await getUserByToken(userToken);

    const invitation = await FriendlyMatchInvitation.create({
      fromClubId,
      toClubId,
      proposedDate,
      proposedStartTime: cleanOptionalString(payload.startTime),
      proposedMatchName,
      message: cleanOptionalString(payload.message),
      status: 'pending',
      createdByUserId: user?.id || null,
    });

    this._sendInvitationEmails(invitation).catch((error) => {
      console.error('[friendly-match-invitation] email send failed:', error?.message || error);
    });

    return toInvitationDto(invitation);
  }

  async listIncomingInvitations(userToken, clubId) {
    await checkAccess(userToken, clubId);
    const items = await FriendlyMatchInvitation.findAll({
      where: { toClubId: clubId },
      order: [['createdAt', 'DESC']],
    });
    return items.map(toInvitationDto);
  }

  async listOutgoingInvitations(userToken, clubId) {
    await checkAccess(userToken, clubId);
    const items = await FriendlyMatchInvitation.findAll({
      where: { fromClubId: clubId },
      order: [['createdAt', 'DESC']],
    });
    return items.map(toInvitationDto);
  }

  async acceptInvitation(userToken, clubId, invitationId) {
    await checkAccess(userToken, clubId);

    const invitation = await FriendlyMatchInvitation.findOne({
      where: { id: invitationId, toClubId: clubId, status: 'pending' },
    });
    if (!invitation) {
      throw new HttpError('Einladung nicht gefunden.', 404);
    }

    const user = await getUserByToken(userToken);

    const fromClub = await Club.findByPk(invitation.fromClubId, { attributes: ['id', 'name'] });
    const toClub = await Club.findByPk(invitation.toClubId, { attributes: ['id', 'name'] });

    const shared = await FriendlyMatchShared.create({
      homeClubId: invitation.fromClubId,
      guestClubId: invitation.toClubId,
      date: invitation.proposedDate,
      startTime: invitation.proposedStartTime,
      matchName: invitation.proposedMatchName,
      homeTeamName: fromClub?.name || 'Heim',
      guestTeamName: toClub?.name || 'Gast',
      matchSystem: 'Braunschweiger System',
      singlesCount: 12,
      doublesCount: 4,
      winningSets: 3,
      homeMatchPoints: 0,
      guestMatchPoints: 0,
      isCompleted: false,
      homeParticipants: [],
      guestParticipants: [],
      resultDetails: [],
      playersReady: [],
      playersPlanned: [],
      playersPlayed: [],
      status: 'active',
      createdByUserId: user?.id || null,
      createdFromInvitationId: invitation.id,
    });

    await invitation.update({
      status: 'accepted',
      acceptedByUserId: user?.id || null,
      acceptedAt: new Date(),
    });

    return {
      invitation: toInvitationDto(invitation),
      sharedMatch: toSharedScheduleRow(shared, clubId),
    };
  }

  async declineInvitation(userToken, clubId, invitationId) {
    await checkAccess(userToken, clubId);

    const invitation = await FriendlyMatchInvitation.findOne({
      where: { id: invitationId, toClubId: clubId, status: 'pending' },
    });
    if (!invitation) {
      throw new HttpError('Einladung nicht gefunden.', 404);
    }

    const dto = toInvitationDto(invitation);
    await invitation.destroy();
    return dto;
  }

  async _sendInvitationEmails(invitation) {
    const [fromClub, toClub] = await Promise.all([
      Club.findByPk(invitation.fromClubId, { attributes: ['id', 'name'] }),
      Club.findByPk(invitation.toClubId, { attributes: ['id', 'name'] }),
    ]);

    if (!toClub) return;

    const recipients = await UserClub.findAll({
      where: {
        clubId: invitation.toClubId,
        approved: true,
      },
      include: [
        {
          model: User,
          as: 'user',
          attributes: ['id', 'email'],
        },
      ],
    });

    const targetEmails = recipients
      .map((row) => row?.user?.email)
      .filter((email) => typeof email === 'string' && email.trim().length > 3);

    if (!targetEmails.length) return;

    await sendFriendlyMatchInvitationEmail({
      toEmails: targetEmails,
      fromClubName: fromClub?.name || `Verein ${invitation.fromClubId}`,
      toClubName: toClub?.name || `Verein ${invitation.toClubId}`,
      proposedDate: invitation.proposedDate,
      proposedStartTime: invitation.proposedStartTime,
      proposedMatchName: invitation.proposedMatchName,
      message: invitation.message,
    });
  }
}

export default new FriendlyMatchSharedService();
