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

const CLICKTT_ENTRY_URL = 'https://httv.click-tt.de/';
const CLICKTT_LOGIN_URL = 'https://httv.click-tt.de/cgi-bin/WebObjects/nuLigaTTDE.woa/wa/login?federation=HeTTV&region=DE';
const TRACE_LIMIT = 250;

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 sanitizePageText(text) {
  return String(text || '').replace(/\s+/g, ' ').trim().slice(0, 500);
}

function sanitizePayload(payload) {
  return String(payload || '')
    .replace(/(password|passwd|pwd|token|secret|captcha)=([^&\s]+)/gi, '$1=[redacted]')
    .replace(/\s+/g, ' ')
    .trim()
    .slice(0, 1500);
}

function escapeRegExp(value) {
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

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

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

class ClickTtPlayerRegistrationService {
  async submitExistingPlayerApplication({ userToken, userId, clubId, memberId }) {
    await checkAccess(userToken, clubId);

    const member = await Member.findOne({
      where: { id: memberId, clubId },
      include: [{ association: 'contacts', required: false }]
    });
    if (!member) {
      throw new HttpError('Mitglied nicht gefunden', 404);
    }

    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. Bitte in den Vereinseinstellungen unter "Verbands-Mitgliedsnummer" eintragen.`,
        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 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);
    }

    const username = account.username;
    const memberJson = member.toJSON();
    const primaryEmail = this._getPrimaryContactValue(memberJson, 'email') || memberJson.email || '';

    if (!primaryEmail) {
      throw new HttpError('Für den Antrag wird eine E-Mail-Adresse beim Mitglied benötigt', 400);
    }

    let browser = null;
    let context = null;
    let page = null;
    const trace = [];
    let selectedSearchResult = null;
    let lastSubmitResult = 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();
      this._attachNetworkLogging(page, trace);

      await this._openAuthenticatedClickTt(page, { username, password, trace });
      await this._selectClubContext(page, associationMemberNumber, trace);
      await this._openApplicationEntry(page, trace);
      await this._fillSearchForm(page, memberJson, trace);
      await this._clickByText(page, 'Personen suchen', trace);
      await page.waitForLoadState('domcontentloaded');

      selectedSearchResult = await this._openApplicationAfterSearch(page, memberJson, trace);
      await this._fillApplicationForm(page, memberJson, primaryEmail, trace);
      await this._clickByText(page, 'Weiter >>', trace);
      await page.waitForLoadState('domcontentloaded');
      lastSubmitResult = await this._buildLastSubmitResult(page, 'Weiter >>');
      await this._dismissConsentOverlays(page, trace);
      await this._clickByText(page, 'Speichern', trace);
      await page.waitForLoadState('domcontentloaded');
      lastSubmitResult = await this._buildLastSubmitResult(page, 'Speichern');
      await this._clickByText(page, 'Einreichen', trace);
      await page.waitForLoadState('domcontentloaded');
      lastSubmitResult = await this._buildLastSubmitResult(page, 'Einreichen');

      const finalText = sanitizePageText(await page.locator('body').innerText());
      const storageState = await context.storageState();
      account.playwrightStorageState = storageState;
      await account.save({ fields: ['playwrightStorageState'] });
      this._trace(trace, 'final', {
        url: page.url(),
        text: finalText
      });

      return {
        success: true,
        message: `Spielberechtigung fuer ${memberJson.firstName} ${memberJson.lastName} wurde in click-TT eingereicht.`,
        finalUrl: page.url(),
        finalText,
        details: {
          selectedSearchResult,
          lastSubmitResult
        }
      };
    } catch (error) {
      const diagnostics = {
        url: null,
        text: null,
        traceTail: trace.slice(-25),
        htmlPath: null,
        selectedSearchResult,
        lastSubmitResult
      };

      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 {
        const bodyText = await page?.locator?.('body')?.innerText?.();
        diagnostics.text = sanitizePageText(bodyText);
      } catch (_err) {
        // ignore
      }

      this._trace(trace, 'error', {
        message: error?.message || String(error),
        url: diagnostics.url || null,
        htmlPath: diagnostics.htmlPath || null
      });
      const message = error instanceof HttpError
        ? error.message
        : `Click-TT-Automatisierung fehlgeschlagen: ${error.message || error}`;
      const wrappedError = new HttpError(`${message}${diagnostics.url ? ` (Seite: ${diagnostics.url})` : ''}${diagnostics.htmlPath ? ` (HTML: ${diagnostics.htmlPath})` : ''}${diagnostics.text ? ` - ${diagnostics.text}` : ''}`, error.statusCode || error.status || 500);
      wrappedError.trace = diagnostics.traceTail || [];
      wrappedError.htmlPath = diagnostics.htmlPath || null;
      wrappedError.details = {
        selectedSearchResult: diagnostics.selectedSearchResult || null,
        lastSubmitResult: diagnostics.lastSubmitResult || null
      };
      throw wrappedError;
    } finally {
      if (context) {
        try {
          await context.close();
        } catch (_err) {
          // ignore
        }
      }
      if (browser) {
        try {
          await browser.close();
        } catch (_err) {
          // ignore
        }
      }
    }
  }

  _getPrimaryContactValue(member, type) {
    const contacts = Array.isArray(member.contacts) ? member.contacts : [];
    const matching = contacts.filter(contact => contact?.type === type && contact?.value);
    const primary = matching.find(contact => contact.isPrimary);
    return (primary || matching[0] || {}).value || '';
  }

  _trace(trace, type, data = {}) {
    const entry = {
      ts: new Date().toISOString(),
      type,
      ...data
    };
    trace.push(entry);
    if (trace.length > TRACE_LIMIT) {
      trace.shift();
    }
    console.log(`[ClickTT Playwright] ${type}`, JSON.stringify(entry));
  }

  _attachNetworkLogging(page, trace) {
    page.on('framenavigated', (frame) => {
      if (frame === page.mainFrame()) {
        this._trace(trace, 'navigate', { url: frame.url() });
      }
    });
    page.on('request', (request) => {
      const resourceType = request.resourceType();
      const method = request.method();
      if (!['document', 'xhr', 'fetch'].includes(resourceType) && method === 'GET') {
        return;
      }
      this._trace(trace, 'request', {
        method,
        resourceType,
        url: request.url(),
        postData: sanitizePayload(request.postData() || '')
      });
    });
    page.on('response', async (response) => {
      const request = response.request();
      const resourceType = request.resourceType();
      const method = request.method();
      if (!['document', 'xhr', 'fetch'].includes(resourceType) && method === 'GET') {
        return;
      }
      let bodySnippet = '';
      if (resourceType === 'document') {
        try {
          bodySnippet = sanitizePageText(await response.text());
        } catch (_err) {
          bodySnippet = '';
        }
      }
      this._trace(trace, 'response', {
        method,
        resourceType,
        url: response.url(),
        status: response.status(),
        bodySnippet
      });
    });
  }

  async _openAuthenticatedClickTt(page, { username, password, trace }) {
    this._trace(trace, 'step', { name: 'open-entry', url: CLICKTT_ENTRY_URL });
    await page.goto(CLICKTT_ENTRY_URL, { waitUntil: 'domcontentloaded', timeout: 45000 });

    await this._dismissConsentOverlays(page, trace);

    const directLoginOrAuth = page.locator('input[name="email"], input[name="username"], input[name="password"]').first();
    if (!(await directLoginOrAuth.count())) {
      this._trace(trace, 'step', { name: 'open-login-entry', url: CLICKTT_LOGIN_URL });
      await page.goto(CLICKTT_LOGIN_URL, { waitUntil: 'domcontentloaded', timeout: 45000 });
      await this._dismissConsentOverlays(page, trace);
    }

    const needsLogin = await page.locator('input[name="email"], input[name="username"], input[name="password"]').count();
    if (!needsLogin) {
      this._trace(trace, 'step', { name: 'session-restored', url: page.url() });
      await page.waitForURL(/click-tt\.de/, { timeout: 45000 });
      return;
    }

    if (!password) {
      throw new HttpError('Die gespeicherte Click-TT-/TTDE-Session ist abgelaufen und es ist kein Passwort gespeichert', 400);
    }

    this._trace(trace, 'step', { name: 'fill-login-form' });
    const userFieldFilled = await this._fillFirstAvailable(page, [
      'input[name="email"]',
      'input[name="username"]',
      'input[type="email"]',
      'input[autocomplete="username"]',
      'input[placeholder*="Username"]',
      'input[placeholder*="E-Mail"]',
      'input[placeholder*="Email"]'
    ], username);
    if (!userFieldFilled) {
      throw new HttpError('TTDE-Loginfeld fuer Benutzername/E-Mail nicht gefunden', 500);
    }

    const passwordFieldFilled = await this._fillFirstAvailable(page, [
      'input[name="password"]',
      'input[type="password"]',
      'input[autocomplete="current-password"]'
    ], password);
    if (!passwordFieldFilled) {
      throw new HttpError('TTDE-Passwortfeld nicht gefunden', 500);
    }

    const submitLocator = page.locator('button[type="submit"], input[type="submit"]').first();
    this._trace(trace, 'step', { name: 'submit-login' });
    await submitLocator.click();
    await page.waitForURL(/click-tt\.de/, { timeout: 60000 });
  }

  async _dismissConsentOverlays(page, trace) {
    const candidates = [
      page.getByRole('button', { name: /verstanden/i }).first(),
      page.getByRole('button', { name: /akzeptieren|accept|zustimmen/i }).first(),
      page.getByRole('link', { name: /verstanden/i }).first(),
      page.locator('#cmpwelcomebtnyes').first(),
      page.locator('[id*="cmp"][class*="button"]:has-text("Akzeptieren")').first(),
      page.locator('button:has-text("Akzeptieren")').first(),
      page.locator('a:has-text("Verstanden")').first()
    ];

    for (const locator of candidates) {
      try {
        if (await locator.count()) {
          this._trace(trace, 'step', { name: 'dismiss-consent-banner' });
          await locator.click({ timeout: 2000 });
          await page.waitForTimeout(300);
          return true;
        }
      } catch (_error) {
        // ignore and continue with next selector
      }
    }

    return false;
  }

  async _openApplicationEntry(page, trace) {
    await this._dismissConsentOverlays(page, trace);

    if (await page.locator('form.search-query').count()) {
      return true;
    }

    const labels = [
      'Spielberechtigungen beantragen',
      'Spielberechtigungen'
    ];

    for (const label of labels) {
      if (await this._hasTextTarget(page, label)) {
        await this._clickByText(page, label, trace);
        await page.waitForLoadState('domcontentloaded');
        await this._dismissConsentOverlays(page, trace);
        break;
      }
    }

    if (await page.locator('form.search-query').count()) {
      return true;
    }

    if (await this._hasTextTarget(page, 'Spielberechtigungen beantragen...')) {
      await this._clickByText(page, 'Spielberechtigungen beantragen...', trace);
      await page.waitForLoadState('domcontentloaded');
      await this._dismissConsentOverlays(page, trace);
      return true;
    }

    if (await page.locator('form.search-query').count()) {
      return true;
    }

    throw new HttpError('Click-TT-Element nicht gefunden: Spielberechtigungen / Spielberechtigungen beantragen...', 500);
  }

  async _selectClubContext(page, associationMemberNumber, trace) {
    await this._dismissConsentOverlays(page, trace);

    const clubLink = page.locator(`a:has-text("(${associationMemberNumber})")`).first();
    if (await clubLink.count()) {
      this._trace(trace, 'step', {
        name: 'select-club-context',
        associationMemberNumber
      });
      await clubLink.click();
      await page.waitForLoadState('domcontentloaded');
      await this._dismissConsentOverlays(page, trace);
      return true;
    }

    this._trace(trace, 'step', {
      name: 'club-context-not-found-on-page',
      associationMemberNumber,
      url: page.url()
    });
    return false;
  }

  async _fillSearchForm(page, member, trace) {
    const searchForm = page.locator('form.search-query').first();
    if (!(await searchForm.count())) {
      throw new HttpError('Click-TT-Suchformular für Spielberechtigungen nicht gefunden', 500);
    }

    this._trace(trace, 'step', { name: 'fill-search-form' });

    await searchForm.locator('input[name$=".1"]').first().fill(member.lastName);
    await searchForm.locator('input[name$=".3"]').first().fill(member.firstName);
    await searchForm.locator('input[name$=".5"]').first().fill(formatGermanDate(member.birthDate));
  }

  async _fillApplicationForm(page, member, email, trace) {
    const form = page.locator('form.edit-object[enctype="multipart/form-data"]').first();
    if (!(await form.count())) {
      throw new HttpError('Click-TT-Antragsformular nicht gefunden', 500);
    }

    this._trace(trace, 'step', { name: 'fill-application-form' });

    const birthDate = formatGermanDate(member.birthDate);
    await this._fillIfEmpty(form.locator('input[name$=".15"]').first(), member.lastName);
    await this._fillIfEmpty(form.locator('input[name$=".17"]').first(), member.firstName);
    await this._fillIfEmpty(form.locator('input[name$=".25"]').first(), birthDate);
    await this._fillIfEmpty(form.locator('input[name$=".9"]').first(), member.street || '');
    await this._fillIfEmpty(form.locator('input[name$=".15"]').nth(1), member.postalCode || '');
    await this._fillIfEmpty(form.locator('input[name$=".17"]').nth(1), member.city || '');
    await this._fillIfEmpty(form.locator('input[name$=".5"]').last(), email || '');

    const nationalitySelectors = [
      'select[name$=".31.1.1.5.13"]:not([disabled])',
      'select[name$=".27"]:not([disabled])'
    ];
    for (const selector of nationalitySelectors) {
      const nationalitySelect = form.locator(selector).first();
      if (!(await nationalitySelect.count())) {
        continue;
      }

      const currentValue = await nationalitySelect.inputValue().catch(() => 'WONoSelectionString');
      if (!currentValue || currentValue === 'WONoSelectionString' || currentValue === '209') {
        await this._selectOptionByLabelContains(nationalitySelect, 'Deutschland');
      }
      break;
    }

    const countrySelect = form.locator('select[name$=".21.3"]').first();
    if (await countrySelect.count()) {
      const currentValue = await countrySelect.inputValue().catch(() => 'WONoSelectionString');
      if (!currentValue || currentValue === 'WONoSelectionString') {
        await this._selectOptionByLabelContains(countrySelect, 'Deutschland');
      }
    }

    const genderMap = {
      female: 'input[id^="radioGenderFemale_"]:not([disabled])',
      male: 'input[id^="radioGenderMale_"]:not([disabled])',
      diverse: 'input[id^="radioGenderNonBinary_"]:not([disabled])',
      unknown: 'input[id^="radioGenderNotSpecified_"]:not([disabled])'
    };
    const genderSelector = genderMap[member.gender] || genderMap.unknown;
    const genderRadio = form.locator(genderSelector).first();
    if (await genderRadio.count()) {
      await genderRadio.check();
    }

    const dataPrivacyCheckbox = form.locator('input[type="checkbox"][name$=".45.3.2"]').first();
    if (await dataPrivacyCheckbox.count()) {
      await dataPrivacyCheckbox.check();
    }
  }

  async _openApplicationAfterSearch(page, member, trace) {
    const expectedName = normalizeComparableText(`${member.lastName}, ${member.firstName}`);
    const expectedBirthDate = formatGermanDate(member.birthDate);

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

      for (const row of rows) {
        const link = row.querySelector('a');
        const strong = row.querySelector('strong');
        const cells = Array.from(row.querySelectorAll('td'));
        const linkText = normalize(link?.textContent || '');
        const nameText = normalize(strong?.textContent || '');
        const birthDateText = normalize(cells[1]?.textContent || '');

        if (!link || !strong) continue;
        if (!linkText.includes('spielberechtigung beantragen')) continue;
        if (nameText !== criteria.expectedName) continue;
        if (birthDateText !== normalize(criteria.expectedBirthDate)) continue;

        return {
          href: link.getAttribute('href'),
          name: String(strong.textContent || '').replace(/\s+/g, ' ').trim(),
          birthDate: String(cells[1]?.textContent || '').replace(/\s+/g, ' ').trim(),
          section: String(row.closest('table')?.querySelector('h2')?.textContent || '').replace(/\s+/g, ' ').trim()
        };
      }

      return null;
    }, {
      expectedName,
      expectedBirthDate
    });

    if (selectedResult?.href) {
      const explicitApplicationLink = page.locator(`a[href="${selectedResult.href}"]`).first();
      let dialogSeen = false;
      page.once('dialog', async (dialog) => {
        dialogSeen = true;
        this._trace(trace, 'dialog', {
          kind: dialog.type(),
          message: sanitizePageText(dialog.message())
        });
        await dialog.accept();
      });

      this._trace(trace, 'step', {
        name: 'click',
        label: 'Spielberechtigung beantragen',
        selector: `a[href="${selectedResult.href}"]`,
        expectedName: `${member.lastName}, ${member.firstName}`,
        expectedBirthDate,
        selectedName: selectedResult.name,
        selectedBirthDate: selectedResult.birthDate
      });
      await explicitApplicationLink.click();
      await page.waitForLoadState('domcontentloaded');

      this._trace(trace, 'step', {
        name: 'search-result-application-opened',
        dialogSeen,
        url: page.url()
      });
      return selectedResult;
    }

    const searchResultsText = sanitizePageText(await page.locator('table.result-set').innerText().catch(() => ''));
    if (searchResultsText) {
      throw new HttpError(
        `Kein exakt passender Click-TT-Sucheintrag für "${member.lastName}, ${member.firstName}" mit Geburtsdatum ${expectedBirthDate} gefunden`,
        409
      );
    }
  }

  async _buildLastSubmitResult(page, action) {
    return {
      action,
      url: page.url(),
      applicant: await this._readCurrentApplicant(page),
      pageText: sanitizePageText(await page.locator('body').innerText().catch(() => ''))
    };
  }

  async _readCurrentApplicant(page) {
    return page.evaluate(() => {
      const form = document.querySelector('form.edit-object');
      if (!form) {
        return null;
      }

      const readValue = (selector) => {
        const field = form.querySelector(selector);
        if (!field) return '';
        if (field.tagName === 'SELECT') {
          const selected = field.options?.[field.selectedIndex];
          return String(selected?.textContent || '').replace(/\s+/g, ' ').trim();
        }
        return String(field.value || '').replace(/\s+/g, ' ').trim();
      };

      const lastName = readValue('input[name$=".15"], input[name$=".31.1.1.5.3"]');
      const firstName = readValue('input[name$=".17"], input[name$=".31.1.1.5.5"]');
      const birthDate = readValue('input[name$=".25"], input[name$=".31.1.1.5.11"]');

      if (!lastName && !firstName && !birthDate) {
        return null;
      }

      return {
        lastName,
        firstName,
        birthDate
      };
    }).catch(() => null);
  }

  async _fillFirstAvailable(page, selectors, value) {
    for (const selector of selectors) {
      const locator = page.locator(selector).first();
      if (await locator.count()) {
        await locator.fill(value);
        return true;
      }
    }
    return false;
  }

  async _fillNthTextInput(page, index, value) {
    if (!value) return false;
    const locator = page.locator('input[type="text"]').nth(index);
    if (await locator.count()) {
      await locator.fill(value);
      return true;
    }
    return false;
  }

  async _fillIfEmpty(locator, value) {
    if (!value || !(await locator.count())) {
      return false;
    }
    const currentValue = (await locator.inputValue().catch(() => '')).trim();
    if (!currentValue) {
      await locator.fill(value);
    }
    return true;
  }

  async _selectOptionByLabelContains(locator, text) {
    if (!(await locator.count())) {
      return false;
    }

    const optionValue = await locator.evaluate((select, searchText) => {
      const normalizedSearch = String(searchText || '').toLowerCase();
      const option = Array.from(select.options).find((entry) =>
        String(entry.textContent || '').toLowerCase().includes(normalizedSearch)
      );
      return option?.value || null;
    }, text);

    if (!optionValue) {
      return false;
    }

    await locator.selectOption(optionValue);
    return true;
  }

  async _hasTextTarget(page, text) {
    const escaped = escapeRegExp(text);
    const selectors = [
      `input[type="submit"][value*="${text}"]`,
      `input[type="button"][value*="${text}"]`,
      `button:has-text("${text}")`,
      `a:has-text("${text}")`,
      `text=/${escaped}/i`
    ];

    for (const selector of selectors) {
      const locator = page.locator(selector).first();
      if (await locator.count()) {
        return true;
      }
    }

    return false;
  }

  async _clickByText(page, text, trace) {
    const escaped = escapeRegExp(text);
    const selectors = [
      `input[type="submit"][value*="${text}"]`,
      `input[type="button"][value*="${text}"]`,
      `button:has-text("${text}")`,
      `a:has-text("${text}")`,
      `text=/${escaped}/i`
    ];

    for (const selector of selectors) {
      const locator = page.locator(selector).first();
      if (await locator.count()) {
        this._trace(trace, 'step', {
          name: 'click',
          label: text,
          selector
        });
        await locator.click();
        return true;
      }
    }

    throw new HttpError(`Click-TT-Element nicht gefunden: ${text}`, 500);
  }
}

export default new ClickTtPlayerRegistrationService();
