import { Injectable, inject } from '@angular/core';
import { BootstrapConfig } from '../../../shared/models/config';
import { ProjectEditorSectionId } from '../models/project-editor.model';
import { WidgetManifestService } from '../../../widgets/registry/widget-manifest.service';
import {
  extractStyleBlocks,
  isValidEmail,
  isValidHexColor,
  isValidHttpUrl,
  normalizeRoute,
  validateAgainstSchemaLite,
  validateCss,
} from '../schema/validators/primitives';

export type ValidationSeverity = 'error' | 'warning';

export interface ProjectValidationIssue {
  /** Stable machine code, kept for back-compat across the sprint. */
  code: string;
  /** i18n key for the human-facing message. */
  message: string;
  /** Editor section the issue belongs to (drives per-section badges). */
  section?: ProjectEditorSectionId;
  /** Schema field key the issue maps to (drives inline field errors). */
  fieldKey?: string;
  /** `error` blocks publishing; `warning` is advisory. */
  severity: ValidationSeverity;
}

const KNOWN_PLATFORM_LAYOUT_TYPES = new Set(['default', 'sidebar-left', 'carousel-home', 'minimal']);
const KNOWN_SECTION_LAYOUT_STRATEGIES = new Set(['stack', 'grid', 'hero', 'carousel', 'split']);

function error(code: string, message: string, section: ProjectEditorSectionId, fieldKey?: string): ProjectValidationIssue {
  return { code, message, section, fieldKey, severity: 'error' };
}

function warning(code: string, message: string, section: ProjectEditorSectionId, fieldKey?: string): ProjectValidationIssue {
  return { code, message, section, fieldKey, severity: 'warning' };
}

/**
 * Centralized, schema-aware validator. Composes the pure primitives under
 * `schema/validators/` and tags every issue with a section + field key so the
 * UI can surface it inline and gate publishing on `severity === 'error'`.
 */
@Injectable({ providedIn: 'root' })
export class ProjectValidator {
  private readonly widgetManifest = inject(WidgetManifestService);

  validate(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
    return [
      ...this.brandingIssues(bootstrap),
      ...this.languageIssues(bootstrap),
      ...this.urlIssues(bootstrap),
      ...this.duplicateSlugIssues(bootstrap),
      ...this.duplicateRouteIssues(bootstrap),
      ...this.homepageIssues(bootstrap),
      ...this.widgetConfigIssues(bootstrap),
      ...this.widgetSettingsSchemaIssues(bootstrap),
      ...this.navigationIssues(bootstrap),
      ...this.colorIssues(bootstrap),
      ...this.cssIssues(bootstrap),
      ...this.translationIssues(bootstrap),
      ...this.layoutIssues(bootstrap),
      ...this.footerContactIssues(bootstrap),
      ...this.footerSocialLinkIssues(bootstrap),
      ...this.footerPaymentIconIssues(bootstrap),
    ];
  }

  private brandingIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
    return bootstrap.branding.logoUrl
      ? []
      : [error('missing-logo', 'builder.validationMissingLogo', 'branding', 'branding.logoUrl')];
  }

  private languageIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
    if (bootstrap.localization.supportedLocales.length === 0) {
      return [error('no-languages', 'builder.validationNoLanguages', 'languages', 'localization.supportedLocales')];
    }
    // General's free-text "Default Language" field (unlike Languages' set-default
    // buttons, which only ever target an already-supported locale) can set this
    // to any string. A default locale that isn't itself supported breaks every
    // `label[defaultLocale]` lookup across nav/static-page content.
    if (!bootstrap.localization.supportedLocales.includes(bootstrap.localization.defaultLocale)) {
      return [error('default-locale-not-supported', 'builder.validationDefaultLocaleNotSupported', 'general', 'localization.defaultLocale')];
    }
    return [];
  }

  private urlIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
    const url = bootstrap.tenant.websiteBaseUrl;
    return !url || isValidHttpUrl(url)
      ? []
      : [error('invalid-url', 'builder.validationInvalidUrl', 'general', 'tenant.websiteBaseUrl')];
  }

  private duplicateSlugIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
    const staticPages = bootstrap.staticPages;
    if (!staticPages || Array.isArray(staticPages)) {
      return [];
    }
    const slugs = Object.values(staticPages).map(page =>
      page.slug && page.slug.trim() ? page.slug : (page.route ?? '').replace(/^\//, ''),
    );
    const hasDuplicates = slugs.some((slug, index) => slugs.indexOf(slug) !== index);
    return hasDuplicates
      ? [error('duplicate-slugs', 'builder.validationDuplicateSlugs', 'static-pages', 'staticPages')]
      : [];
  }

  /**
   * Duplicate route ownership: two `pages` claiming the same path, or a page
   * route colliding with a static-page route. Advisory (warning) - a duplicate
   * here is usually a mistake but does not corrupt the config.
   */
  private duplicateRouteIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
    const pageRoutes = bootstrap.pages
      .map(page => normalizeRoute(page.route?.path ?? ''))
      .filter(route => route.length > 0);
    const hasPageDuplicates = pageRoutes.some((route, index) => pageRoutes.indexOf(route) !== index);

    const staticPages = bootstrap.staticPages;
    const staticRoutes = staticPages && !Array.isArray(staticPages)
      ? Object.values(staticPages)
          .map(page => normalizeRoute(page.slug && page.slug.trim() ? page.slug : (page.route ?? '')))
          .filter(route => route.length > 0)
      : [];
    const pageRouteSet = new Set(pageRoutes);
    const collides = staticRoutes.some(route => pageRouteSet.has(route));

    return hasPageDuplicates || collides
      ? [warning('duplicate-routes', 'builder.validationDuplicateRoutes', 'homepage', 'pages')]
      : [];
  }

  private homepageIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
    const homePage = bootstrap.pages.find(page => page.key === 'home' || page.route.path === '/');
    if (!homePage || homePage.sections.length === 0) {
      return [error('empty-homepage', 'builder.validationEmptyHomepage', 'homepage', 'pages')];
    }
    const hasMissingWidgetType = homePage.sections.some(section => section.widgets.some(widget => !widget.type?.trim()));
    return hasMissingWidgetType
      ? [error('missing-widget', 'builder.validationMissingWidget', 'homepage', 'pages')]
      : [];
  }

  /**
   * Structural widget integrity across every page: each widget needs an id, a
   * non-empty type, a version, and a props object. Complements the homepage
   * `missing-widget` check with a config-shape guard the widget engine relies on.
   */
  private widgetConfigIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
    const malformed = bootstrap.pages.some(page =>
      page.sections.some(section =>
        section.widgets.some(widget =>
          !widget.id?.trim() ||
          !widget.type?.trim() ||
          !widget.version?.trim() ||
          widget.props === null ||
          typeof widget.props !== 'object',
        ),
      ),
    );
    return malformed
      ? [error('invalid-widget-config', 'builder.validationInvalidWidgetConfig', 'widgets', 'pages')]
      : [];
  }

  /**
   * Validates each widget's `props` against its manifest entry's `settingsSchema`
   * (basic type/required checks - see validateAgainstSchemaLite). No-ops until
   * the manifest has loaded once (same early-return convention as
   * RuntimeDiagnosticsValidator.validateDatasourceCoverage) rather than blocking
   * publish on a manifest fetch race.
   */
  private widgetSettingsSchemaIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
    const manifest = this.widgetManifest.getManifestSnapshot();
    if (!manifest) {
      return [];
    }
    const manifestByType = new Map(manifest.widgets.map(entry => [entry.type, entry]));

    const hasInvalidSettings = bootstrap.pages.some(page =>
      page.sections.some(section =>
        section.widgets.some(widget => {
          const entry = manifestByType.get(widget.type);
          if (!entry?.settingsSchema || !widget.props || typeof widget.props !== 'object') {
            return false;
          }
          return validateAgainstSchemaLite(widget.props, entry.settingsSchema).length > 0;
        }),
      ),
    );

    return hasInvalidSettings
      ? [warning('invalid-widget-settings', 'builder.validationInvalidWidgetSettings', 'widgets', 'pages')]
      : [];
  }

  private navigationIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
    const keyOf = (item: { label?: string | Record<string, string>; route?: string }): string =>
      `${typeof item.label === 'string' ? item.label : JSON.stringify(item.label ?? {})}|${item.route ?? ''}`;
    const keys = bootstrap.navigation.header.map(keyOf);
    const hasDuplicates = keys.some((key, index) => keys.indexOf(key) !== index);
    return hasDuplicates
      ? [error('duplicate-nav-links', 'builder.validationDuplicateNavLinks', 'navigation', 'navigation.header')]
      : [];
  }

  private colorIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
    return Object.entries(bootstrap.theme.palette)
      .filter(([, value]) => !isValidHexColor(value))
      .map(([key]) => error('invalid-colors', 'builder.validationInvalidColors', 'theme', `theme.palette.${key}`));
  }

  /** Validates CSS inside `<style>` blocks of static-page HTML content. */
  private cssIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
    const staticPages = bootstrap.staticPages;
    if (!staticPages || Array.isArray(staticPages)) {
      return [];
    }
    const htmlStrings: string[] = [];
    for (const page of Object.values(staticPages)) {
      if (typeof page.html === 'string') {
        htmlStrings.push(page.html);
      } else if (page.html && typeof page.html === 'object') {
        htmlStrings.push(...Object.values(page.html));
      }
    }
    const hasInvalidCss = htmlStrings
      .flatMap(html => extractStyleBlocks(html))
      .some(css => !validateCss(css).ok);
    return hasInvalidCss
      ? [warning('invalid-css', 'builder.validationInvalidCss', 'static-pages', 'staticPages')]
      : [];
  }

  private translationIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
    const otherLocales = bootstrap.localization.supportedLocales.filter(locale => locale !== bootstrap.localization.defaultLocale);
    if (otherLocales.length === 0) {
      return [];
    }

    const headerMissing = bootstrap.navigation.header.some(item => {
      const label = item.label;
      if (!label || typeof label !== 'object') {
        return false;
      }
      return otherLocales.some(locale => !label[locale]?.trim());
    });

    const staticPages = bootstrap.staticPages;
    const staticPagesMissing = staticPages && !Array.isArray(staticPages)
      ? Object.values(staticPages).some(page => page.translations && otherLocales.some(locale => !page.translations![locale]))
      : false;

    return headerMissing || staticPagesMissing
      ? [error('missing-translations', 'builder.validationMissingTranslations', 'languages', 'localization.supportedLocales')]
      : [];
  }

  private layoutIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
    const invalidPlatformLayout = !!bootstrap.layout?.type && !KNOWN_PLATFORM_LAYOUT_TYPES.has(bootstrap.layout.type);
    const invalidSectionLayout = bootstrap.pages.some(page =>
      page.sections.some(section => !!section.layout?.strategy && !KNOWN_SECTION_LAYOUT_STRATEGIES.has(section.layout.strategy)),
    );

    return invalidPlatformLayout || invalidSectionLayout
      ? [error('invalid-layouts', 'builder.validationInvalidLayouts', 'theme', 'layout.type')]
      : [];
  }

  private footerContactIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
    const email = bootstrap.company?.contacts?.email;
    return !email || isValidEmail(email)
      ? []
      : [error('invalid-contact-email', 'builder.validationInvalidContactEmail', 'footer', 'company.contacts.email')];
  }

  private footerSocialLinkIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
    const hasInvalidUrl = (bootstrap.footer?.socialLinks ?? []).some(link => !!link.url && !isValidHttpUrl(link.url));
    return hasInvalidUrl
      ? [warning('invalid-social-link-url', 'builder.validationInvalidSocialLinkUrl', 'footer', 'footer.socialLinks')]
      : [];
  }

  /** A payment icon row with only one of src/alt set is a broken image reference or missing accessibility text. */
  private footerPaymentIconIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
    const hasIncompleteRow = (bootstrap.footer?.paymentIcons ?? []).some(icon => !!icon.src !== !!icon.alt);
    return hasIncompleteRow
      ? [warning('incomplete-payment-icon', 'builder.validationIncompletePaymentIcon', 'footer', 'footer.paymentIcons')]
      : [];
  }
}
