import { chromium } from 'playwright';
import { mkdir, writeFile } from 'fs/promises';
import path from 'path';
import Club from '../models/Club.js';
import ClickTtAccount from '../models/ClickTtAccount.js';
import OfficialTournament from '../models/OfficialTournament.js';
import OfficialCompetition from '../models/OfficialCompetition.js';
import OfficialCompetitionMember from '../models/OfficialCompetitionMember.js';
import Member from '../models/Member.js';
import HttpError from '../exceptions/HttpError.js';
import { checkAccess } from '../utils/userUtils.js';
import clickTtPlayerRegistrationService from './clickTtPlayerRegistrationService.js';

function normalizeText(value) {
  return String(value || '')
    .normalize('NFKC')
    .replace(/\s+/g, ' ')
    .trim()
    .toLowerCase();
}

function tokenizeText(value) {
  return normalizeText(value)
    .split(/[^a-z0-9äöüß]+/i)
    .map((token) => token.trim())
    .filter((token) => token.length >= 3)
    .filter((token) => !new Set([
      'und', 'der', 'die', 'das', 'des', 'den', 'dem', 'von', 'für', 'mit',
      'ein', 'eine', 'einer', 'eines', 'zum', 'zur', 'im', 'am', 'an'
    ]).has(token));
}

function getTitleSearchProfile(title) {
  const normalizedTitle = normalizeText(title);
  const tokens = tokenizeText(title);
  const tailTokens = tokens.slice(-2);

  return {
    normalizedTitle,
    tokens,
    tailTokens
  };
}

function formatGermanDate(value) {
  if (!value) return '';
  const date = value instanceof Date ? value : new Date(value);
  if (Number.isNaN(date.getTime())) return '';
  const day = String(date.getDate()).padStart(2, '0');
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const year = String(date.getFullYear());
  return `${day}.${month}.${year}`;
}

function parseDmyRange(value) {
  const matches = String(value || '').match(/(\d{1,2}\.\d{1,2}\.\d{4})/g) || [];
  return {
    from: matches[0] || '',
    to: matches[1] || matches[0] || ''
  };
}

function buildDebugHtmlPath() {
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
  return path.join(process.cwd(), 'tmp', `clicktt-tournament-debug-${timestamp}.html`);
}

function sanitizePageText(text) {
  return String(text || '').replace(/\s+/g, ' ').trim().slice(0, 500);
}

class ClickTtTournamentRegistrationService {
  async autoRegisterPendingParticipants({ userToken, userId, clubId, tournamentId }) {
    await checkAccess(userToken, clubId);

    const club = await Club.findByPk(clubId, {
      attributes: ['id', 'name', 'associationMemberNumber']
    });
    const associationMemberNumber = String(
      club?.associationMemberNumber ??
      club?.association_member_number ??
      club?.dataValues?.associationMemberNumber ??
      club?.dataValues?.association_member_number ??
      ''
    ).trim();
    if (!associationMemberNumber) {
      throw new HttpError(
        `Für den Verein "${club?.name ?? clubId}" ist keine Verbands-Mitgliedsnummer hinterlegt.`,
        400
      );
    }

    const account = await ClickTtAccount.findOne({ where: { userId } });
    if (!account?.username) {
      throw new HttpError('Kein HTTV-/click-TT-Account für diesen Benutzer hinterlegt', 400);
    }

    const tournament = await OfficialTournament.findOne({
      where: { id: tournamentId, clubId },
      include: [{
        model: OfficialCompetition,
        as: 'competitions',
        required: false,
        include: [{
          model: OfficialCompetitionMember,
          as: 'members',
          required: false,
          where: {
            wants: true,
            registered: false,
            participated: false
          },
          include: [{
            model: Member,
            as: 'member',
            required: true
          }]
        }]
      }]
    });

    if (!tournament) {
      throw new HttpError('Offizielles Turnier nicht gefunden', 404);
    }

    const competitionGroups = (tournament.competitions || [])
      .map((competition) => ({
        competition,
        entries: (competition.members || [])
          .filter((entry) => entry.member?.active)
      }))
      .filter((group) => group.entries.length > 0);

    if (!competitionGroups.length) {
      throw new HttpError('Keine offenen Turnieranmeldungen mit Status "möchte teilnehmen" gefunden', 400);
    }

    const password = account.getPassword?.() || null;
    const savedStorageState = account.playwrightStorageState ?? null;
    if (!savedStorageState && !password) {
      throw new HttpError('Für die Click-TT-Automatisierung wird ein gespeichertes HTTV-/click-TT-Passwort oder eine gültige Browser-Session benötigt', 400);
    }

    let browser = null;
    let context = null;
    let page = null;
    const trace = [];
    let resultsPageUrl = null;

    try {
      browser = await chromium.launch({
        headless: true,
        args: ['--no-sandbox', '--disable-dev-shm-usage']
      });
      context = savedStorageState
        ? await browser.newContext({ storageState: savedStorageState })
        : await browser.newContext();
      page = await context.newPage();
      clickTtPlayerRegistrationService._attachNetworkLogging(page, trace);

      await clickTtPlayerRegistrationService._openAuthenticatedClickTt(page, {
        username: account.username,
        password,
        trace
      });
      await clickTtPlayerRegistrationService._selectClubContext(page, associationMemberNumber, trace);
      await this._openTournamentSearch(page, trace);
      await this._filterTournamentSearch(page, tournament, trace);
      resultsPageUrl = page.url();

      const processedCompetitions = [];
      for (const group of competitionGroups) {
        if (page.url() !== resultsPageUrl) {
          clickTtPlayerRegistrationService._trace(trace, 'step', {
            name: 'open-search-results',
            url: resultsPageUrl
          });
          await page.goto(resultsPageUrl, { waitUntil: 'domcontentloaded' });
          await clickTtPlayerRegistrationService._dismissConsentOverlays(page, trace);
        }
        await this._openCompetitionRegistration(page, tournament, group.competition, trace);
        await this._selectCompetitionMembers(page, group.entries, trace);
        await this._openControlStep(page, trace);
        await this._verifyControlPage(page, group.entries);
        await this._saveCompetitionRegistration(page, group.entries, trace);

        for (const entry of group.entries) {
          entry.registered = true;
          await entry.save({ fields: ['registered'] });
        }

        processedCompetitions.push({
          competitionId: group.competition.id,
          competitionName: group.competition.ageClassCompetition || group.competition.altersklasseWettbewerb || '',
          registeredMembers: group.entries.map((entry) => ({
            memberId: entry.memberId,
            name: `${entry.member.firstName} ${entry.member.lastName}`.trim(),
            birthDate: entry.member.birthDate || ''
          }))
        });
      }

      account.playwrightStorageState = await context.storageState();
      await account.save({ fields: ['playwrightStorageState'] });

      return {
        success: true,
        message: `${processedCompetitions.reduce((sum, item) => sum + item.registeredMembers.length, 0)} Teilnehmer wurden automatisch in click-TT angemeldet.`,
        competitions: processedCompetitions
      };
    } catch (error) {
      const diagnostics = {
        url: null,
        text: null,
        htmlPath: null,
        resultsPageUrl
      };

      try {
        const html = await page?.content?.();
        const htmlPath = html ? buildDebugHtmlPath() : null;
        if (html && htmlPath) {
          await mkdir(path.dirname(htmlPath), { recursive: true });
          await writeFile(htmlPath, html, 'utf8');
          diagnostics.htmlPath = htmlPath;
        }
      } catch (_err) {
        // keep partial diagnostics
      }

      try {
        diagnostics.url = page?.url?.() || null;
      } catch (_err) {
        // ignore
      }

      try {
        diagnostics.text = sanitizePageText(await page?.locator?.('body')?.innerText?.());
      } catch (_err) {
        // ignore
      }

      const baseMessage = error instanceof HttpError
        ? error.message
        : `Click-TT-Turnieranmeldung fehlgeschlagen: ${error.message || error}`;
      const wrappedError = new HttpError(
        `${baseMessage}${diagnostics.url ? ` (Seite: ${diagnostics.url})` : ''}${diagnostics.htmlPath ? ` (HTML: ${diagnostics.htmlPath})` : ''}${diagnostics.text ? ` - ${diagnostics.text}` : ''}`,
        error.statusCode || error.status || 500
      );
      wrappedError.htmlPath = diagnostics.htmlPath || null;
      wrappedError.details = {
        resultsPageUrl: diagnostics.resultsPageUrl || null
      };
      throw wrappedError;
    } finally {
      if (context) {
        try { await context.close(); } catch (_err) {}
      }
      if (browser) {
        try { await browser.close(); } catch (_err) {}
      }
    }
  }

  async _openTournamentSearch(page, trace) {
    await clickTtPlayerRegistrationService._dismissConsentOverlays(page, trace);
    await clickTtPlayerRegistrationService._clickByText(page, 'Turniere', trace);
    await page.waitForLoadState('domcontentloaded');
    await clickTtPlayerRegistrationService._dismissConsentOverlays(page, trace);
    await clickTtPlayerRegistrationService._clickByText(page, 'Teilnehmeranmeldung', trace);
    await page.waitForLoadState('domcontentloaded');
    await clickTtPlayerRegistrationService._dismissConsentOverlays(page, trace);
  }

  async _filterTournamentSearch(page, tournament, trace) {
    const federationSelect = page.locator('#federationsPopupButton, select[name="0.1.63.7.5.1"]').first();
    if (await federationSelect.count()) {
      const federationValue = await federationSelect.evaluate((select) => {
        const options = Array.from(select.options || []);
        const match = options.find((option) => /hess|hettv|httv/i.test(String(option.textContent || '')));
        return match?.value || null;
      });
      if (federationValue) {
        await federationSelect.selectOption(federationValue);
      }
    }

    const range = parseDmyRange(tournament.eventDate);
    const fromInput = page.locator('input[name="queryDateFrom"]').first();
    if (await fromInput.count() && range.from) {
      await fromInput.fill(range.from);
    }
    const toInput = page.locator('input[name="queryDateTo"]').first();
    if (await toInput.count() && range.to) {
      await toInput.fill(range.to);
    }

    if (await clickTtPlayerRegistrationService._hasTextTarget(page, 'filtern')) {
      await clickTtPlayerRegistrationService._clickByText(page, 'filtern', trace);
      await page.waitForLoadState('domcontentloaded');
    }
  }

  async _openCompetitionRegistration(page, tournament, competition, trace) {
    const competitionName = String(competition.ageClassCompetition || competition.altersklasseWettbewerb || '').trim();
    if (!competitionName) {
      throw new HttpError('Turnierkonkurrenz ohne Bezeichnung gefunden', 500);
    }

    const titleProfile = getTitleSearchProfile(tournament.title || '');

    const href = await page.locator('#content-row1 tr, #content tr, #container tr').evaluateAll((rows, criteria) => {
      const normalize = (value) => String(value || '')
        .normalize('NFKC')
        .replace(/\s+/g, ' ')
        .trim()
        .toLowerCase();
      const tokenize = (value) => normalize(value)
        .split(/[^a-z0-9äöüß]+/i)
        .map((token) => token.trim())
        .filter((token) => token.length >= 3)
        .filter((token) => !new Set([
          'und', 'der', 'die', 'das', 'des', 'den', 'dem', 'von', 'für', 'mit',
          'ein', 'eine', 'einer', 'eines', 'zum', 'zur', 'im', 'am', 'an'
        ]).has(token));

      const wantedCompetition = normalize(criteria.competitionName);
      const wantedTournament = normalize(criteria.tournamentTitle);
      const wantedTournamentTokens = Array.isArray(criteria.tournamentTokens) ? criteria.tournamentTokens : [];
      const wantedTailTokens = Array.isArray(criteria.tournamentTailTokens) ? criteria.tournamentTailTokens : [];

      let bestHref = null;
      let bestScore = -1;

      for (const row of rows) {
        const rowText = normalize(row.textContent || '');
        if (!rowText.includes(wantedCompetition)) continue;
        if (wantedTournament && !rowText.includes(wantedTournament)) continue;

        const anchor = Array.from(row.querySelectorAll('a[href]')).find((candidate) =>
          /anmeldung|anmelden|melden/i.test(normalize(candidate.textContent || ''))
        );
        if (!anchor) continue;
        const href = anchor.getAttribute('href');
        if (!href) continue;
        if (/dataProtection|legalNotice|logout|contact/i.test(href)) continue;

        const linkText = normalize(anchor.textContent || '');
        const combinedText = `${rowText} ${linkText}`.trim();
        const combinedTokens = new Set(tokenize(combinedText));

        let score = 0;
        if (wantedTournament && combinedText.includes(wantedTournament)) score += 100;
        if (/anmeldung|anmelden|melden/.test(linkText)) score += 20;

        for (const token of wantedTournamentTokens) {
          if (combinedTokens.has(token)) score += 3;
        }

        for (const token of wantedTailTokens) {
          if (combinedTokens.has(token)) score += 8;
        }

        if (score > bestScore) {
          bestScore = score;
          bestHref = href;
        }
      }

      return bestHref;
    }, {
      competitionName,
      tournamentTitle: tournament.title || '',
      tournamentTokens: titleProfile.tokens,
      tournamentTailTokens: titleProfile.tailTokens
    });

    if (!href) {
      throw new HttpError(`Click-TT-Turnierkonkurrenz nicht gefunden: ${competitionName}`, 500);
    }

    clickTtPlayerRegistrationService._trace(trace, 'step', {
      name: 'click',
      label: competitionName,
      selector: `a[href="${href}"]`
    });
    await page.locator(`#content-row1 a[href="${href}"], #content a[href="${href}"], #container a[href="${href}"]`).first().click();
    await page.waitForLoadState('domcontentloaded');
  }

  async _selectCompetitionMembers(page, entries, trace) {
    const unmatched = [];

    for (const entry of entries) {
      const expectedRow = {
        fullName: normalizeText(`${entry.member.lastName}, ${entry.member.firstName}`),
        fullNameAlt: normalizeText(`${entry.member.firstName} ${entry.member.lastName}`),
        birthDate: formatGermanDate(entry.member.birthDate)
      };

      const matched = await page.locator('tr').evaluateAll((rows, criteria) => {
        const normalize = (value) => String(value || '')
          .normalize('NFKC')
          .replace(/\s+/g, ' ')
          .trim()
          .toLowerCase();

        for (let index = 0; index < rows.length; index += 1) {
          const row = rows[index];
          const rowText = normalize(row.textContent || '');
          if (!rowText.includes(criteria.fullName) && !rowText.includes(criteria.fullNameAlt)) continue;
          if (criteria.birthDate && !rowText.includes(normalize(criteria.birthDate))) continue;
          return index;
        }
        return -1;
      }, expectedRow);

      if (matched < 0) {
        unmatched.push(`${entry.member.firstName} ${entry.member.lastName} (${expectedRow.birthDate})`);
        continue;
      }

      const row = page.locator('tr').nth(matched);
      const checkbox = row.locator('input[type="checkbox"]:not([disabled])').first();
      if (await checkbox.count()) {
        if (!(await checkbox.isChecked().catch(() => false))) {
          await checkbox.check();
        }
        continue;
      }

      const registerButton = row.locator('input[type="submit"], button, a').filter({ hasText: /anmelden|hinzufügen|melden/i }).first();
      if (await registerButton.count()) {
        await registerButton.click();
        await page.waitForLoadState('domcontentloaded');
        continue;
      }

      unmatched.push(`${entry.member.firstName} ${entry.member.lastName} (${expectedRow.birthDate})`);
    }

    if (unmatched.length) {
      throw new HttpError(`Folgende Teilnehmer konnten auf der Click-TT-Turnierseite nicht ausgewählt werden: ${unmatched.join(', ')}`, 500);
    }
  }

  async _openControlStep(page, trace) {
    if (await clickTtPlayerRegistrationService._hasTextTarget(page, 'Weiter >>')) {
      await clickTtPlayerRegistrationService._clickByText(page, 'Weiter >>', trace);
      await page.waitForLoadState('domcontentloaded');
      return;
    }

    if (normalizeText(await page.locator('body').innerText().catch(() => '')).includes('folgende spieler werden zum turnier angemeldet')) {
      return;
    }

    throw new HttpError('Click-TT-Kontrollseite für die Turnieranmeldung nicht erreicht', 500);
  }

  async _verifyControlPage(page, entries) {
    const bodyText = normalizeText(await page.locator('body').innerText().catch(() => ''));
    for (const entry of entries) {
      const name = normalizeText(`${entry.member.lastName}, ${entry.member.firstName}`);
      const birthDate = formatGermanDate(entry.member.birthDate);
      if (!bodyText.includes(name) || (birthDate && !bodyText.includes(normalizeText(birthDate)))) {
        throw new HttpError(`Kontrollseite der Click-TT-Turnieranmeldung enthält nicht den erwarteten Teilnehmer ${entry.member.firstName} ${entry.member.lastName}`, 500);
      }
    }
  }

  async _saveCompetitionRegistration(page, entries, trace) {
    await clickTtPlayerRegistrationService._clickByText(page, 'Speichern', trace);
    await page.waitForLoadState('domcontentloaded');

    const bodyText = normalizeText(await page.locator('body').innerText().catch(() => ''));
    const stillOnControlPage = bodyText.includes('folgende spieler werden zum turnier angemeldet')
      || bodyText.includes('turnier-teilnehmeranmeldung')
      || await page.locator('input[type="submit"][value="Speichern"]').count().catch(() => 0);

    if (stillOnControlPage) {
      const expectedNames = entries.map((entry) => `${entry.member.firstName} ${entry.member.lastName}`);
      throw new HttpError(
        `Click-TT-Turnieranmeldung wurde nach "Speichern" nicht bestätigt: ${expectedNames.join(', ')}`,
        500
      );
    }
  }
}

export default new ClickTtTournamentRegistrationService();
