/**
 * RRYM / 002 — SULSYS
 * Zod schemas for every content type. Content is the single source of truth and
 * is validated at load time, so a missing or malformed field fails the build
 * rather than rendering an empty page.
 *
 * Production Blueprint v1, section 12 ("Content") and section 17 rule 1:
 * an unknown value renders nothing — never "TBD", never a placeholder number.
 */

import { z } from "zod";
import { LOCALES } from "./locales";
import { MODULE_IDS, SECTOR_IDS, type ModuleId, type SectorId } from "./routes";

/* -------------------------------------------------------------------------- */
/* Primitives                                                                 */
/* -------------------------------------------------------------------------- */

const nonEmpty = z.string().trim().min(1);

/*
 * Keep the literal unions through validation: casting to a non-empty readonly
 * tuple lets `z.infer` produce `ModuleId` / `SectorId` rather than widening to
 * `string`, so a content file can never carry an id the route registry does
 * not know.
 */
const moduleId = z.enum(MODULE_IDS as readonly [ModuleId, ...ModuleId[]]);
const sectorId = z.enum(SECTOR_IDS as readonly [SectorId, ...SectorId[]]);

export const localeSchema = z.enum(LOCALES as unknown as [string, ...string[]]);

/* -------------------------------------------------------------------------- */
/* facts.json                                                                 */
/* -------------------------------------------------------------------------- */

/**
 * Every claim the site can make, with its evidence. `verified: false` means the
 * claim exists in research but has not been confirmed for publication — the
 * renderer must emit nothing for it. This is the mechanism that keeps the site
 * honest after handover (Blueprint 10, "Unknown-data handling").
 */
export const factSchema = z.object({
  verified: z.boolean(),
  /** Where the claim comes from. Required even when unverified. */
  source: nonEmpty,
  /** Evidence strength, mirroring PUBLIC-SOURCE-RESEARCH labels. */
  status: z.enum(["VERIFIED", "HISTORICAL", "HISTORICAL-STRONG", "HISTORICAL-RECENT", "UNKNOWN"]),
  /** A concrete value, when one exists and may be shown. */
  value: z.union([z.string(), z.number()]).optional(),
  /** Date the evidence is from, ISO or year. */
  asOf: z.string().optional(),
  /** Why the fact is gated, for the handover documentation. */
  note: z.string().optional(),
});

export const factsFileSchema = z.record(z.string(), factSchema);

export type Fact = z.infer<typeof factSchema>;
export type FactsFile = z.infer<typeof factsFileSchema>;

/* -------------------------------------------------------------------------- */
/* Shared content fragments                                                   */
/* -------------------------------------------------------------------------- */

/**
 * A row that only renders when its fact is verified. `fact` names a key in
 * facts.json; rows whose fact is unverified are dropped by the loader, so a
 * component never has to know about gating.
 */
const gatedRow = z.object({
  label: nonEmpty,
  value: nonEmpty,
  fact: nonEmpty,
});

const gatedFeature = z.object({
  text: nonEmpty,
  fact: nonEmpty,
});

export type GatedRow = z.infer<typeof gatedRow>;
export type GatedFeature = z.infer<typeof gatedFeature>;

/**
 * One step of the system-of-record chain. `doc` is the document type Sulsys's
 * own copy uses (BLUEPRINT-DELTA D-14).
 */
const chainStep = z.object({
  step: nonEmpty,
  doc: nonEmpty,
  module: moduleId.optional(),
});

export type ChainStep = z.infer<typeof chainStep>;

/* -------------------------------------------------------------------------- */
/* Blueprint sheet — the honest stand-in for a product screen                  */
/* -------------------------------------------------------------------------- */

/**
 * Until sanitised captures from the current Nexys build exist (WP08), the screen
 * slot carries a *blueprint sheet*: the module's real document structure, drawn
 * as a ruled record in the product's own terminology. It is explicitly labelled
 * as a structure diagram, never dressed up as a screenshot — Blueprint 08
 * ("Where a sanitised screen is missing, the slot collapses and the documents
 * table carries the proof. Never a 'coming soon' image.") and the project rule
 * that no product UI may be fabricated.
 *
 * When a real capture arrives it is added as `screen` on the module and the
 * sheet steps aside without any redesign.
 */
export const blueprintSheetSchema = z.object({
  /** Mono header of the sheet, e.g. "NEXYS MAT · ARKA". */
  title: nonEmpty,
  /** What the sheet depicts, one short line. */
  caption: nonEmpty,
  kind: z.enum(["table", "pos", "tables", "ledger"]),
  columns: z.array(nonEmpty).min(2).max(4),
  rows: z.array(z.array(z.string()).min(2).max(4)).min(3).max(12),
  /** Totals line, rendered in green as the "posted" row. */
  total: z.array(z.string()).optional(),
  /** Function-key legend, exactly as evidenced in the product UI. */
  keys: z.array(z.object({ key: nonEmpty, label: nonEmpty })).optional(),
  /** Table-map cells for the hospitality sheet. */
  cells: z.array(z.object({ label: nonEmpty, state: z.enum(["open", "free", "bill"]) })).optional(),
});

export type BlueprintSheet = z.infer<typeof blueprintSheetSchema>;

/** A real, sanitised product capture. None exist yet; the field is ready. */
export const screenSchema = z.object({
  src: nonEmpty,
  width: z.number().int().positive(),
  height: z.number().int().positive(),
  /** Interface language of the capture, named in the caption. */
  uiLanguage: localeSchema,
  caption: nonEmpty,
  alt: nonEmpty,
  orientation: z.enum(["landscape", "portrait"]).default("landscape"),
});

export type Screen = z.infer<typeof screenSchema>;

/* -------------------------------------------------------------------------- */
/* product/nexys.{locale}.json                                               */
/* -------------------------------------------------------------------------- */

export const productSchema = z.object({
  name: nonEmpty,
  oneLiner: nonEmpty,
  intro: z.array(nonEmpty).min(1).max(3),
  /** The chain, trade lane. */
  chain: z.array(chainStep).min(4).max(8),
  /** The chain, hospitality lane (BLUEPRINT-DELTA D-14). */
  chainHospitality: z.array(chainStep).min(2).max(6),
  chainSentence: nonEmpty,
  /** "How it runs" rows — each gated on a fact. */
  runtime: z.array(gatedRow).min(1),
  /** Platform/edition rows (D-02). Nexys Web is gated until confirmed. */
  platforms: z.array(gatedRow),
});

export type Product = z.infer<typeof productSchema>;

/* -------------------------------------------------------------------------- */
/* modules/{id}.{locale}.json                                                */
/* -------------------------------------------------------------------------- */

export const moduleSchema = z.object({
  id: moduleId,
  /** Sulsys's own marketing name — the primary name on the page (D-01). */
  brandName: nonEmpty,
  /** Descriptive name in this locale — the subtitle (D-01). */
  name: nonEmpty,
  oneLiner: nonEmpty,
  /** One sentence of what it records, for the module page title block. */
  records: nonEmpty,
  /**
   * The module's unit of work, two to four of the product's own nouns:
   * Finance counts accounts and vouchers, Caffe counts tables and orders.
   * Printed as a mono triad in the module masthead, it is what makes four
   * pages built from one template read as four different products.
   */
  unit: z.array(nonEmpty).min(2).max(4),
  /** Names this product has carried, so existing users connect them. */
  legacyNames: z.array(nonEmpty).default([]),
  /** M2 — documents table. Only documents seen in screens or videos. */
  documents: z
    .array(
      z.object({
        doc: nonEmpty,
        does: nonEmpty,
        connects: nonEmpty,
      }),
    )
    .min(4)
    .max(14),
  /** M3 — one daily workflow. */
  workflow: z.array(nonEmpty).min(4).max(7),
  /** Capability rows, each gated on a fact. */
  features: z.array(gatedFeature).default([]),
  /** M4 — connected modules. */
  connects: z.array(moduleId).default([]),
  /** M6 — sectors that use this module. */
  sectors: z.array(sectorId).default([]),
  /** M1 — the blueprint sheet standing in for a capture. */
  sheet: blueprintSheetSchema,
  /** A real capture, when one exists. */
  screen: screenSchema.optional(),
});

export type ModuleContent = z.infer<typeof moduleSchema>;

/* -------------------------------------------------------------------------- */
/* sectors/{id}.{locale}.json                                                */
/* -------------------------------------------------------------------------- */

export const sectorSchema = z.object({
  id: sectorId,
  /**
   * Whether the page ships. Blueprint 03 gates each sector on Kujto's
   * confirmation; the four clusters here are each evidence-backed
   * (PUBLIC-SOURCE-RESEARCH 09) and D-04 keeps all four, so they ship with
   * verified product capability only — no install counts, no client names.
   */
  enabled: z.boolean(),
  name: nonEmpty,
  /** Who it is for — names the typical business. */
  forWhom: nonEmpty,
  intro: nonEmpty,
  /** 4–6 numbered steps in operational language, mapped to modules. */
  flow: z
    .array(
      z.object({
        step: nonEmpty,
        module: moduleId.optional(),
      }),
    )
    .min(4)
    .max(6),
  modules: z.array(moduleId).min(1),
  sheet: blueprintSheetSchema,
});

export type SectorContent = z.infer<typeof sectorSchema>;

/** Sulsys's own 19-item business-type list, verbatim (D-04). */
export const businessTypesSchema = z.object({
  /** Lead-in making clear it is Sulsys's own list. */
  lead: nonEmpty,
  items: z.array(nonEmpty).min(10),
  source: nonEmpty,
});

export type BusinessTypes = z.infer<typeof businessTypesSchema>;

/* -------------------------------------------------------------------------- */
/* clients/clients.json                                                       */
/* -------------------------------------------------------------------------- */

/**
 * Twenty references exist in public evidence. None may be named until written
 * permission exists (Blueprint 17 rule 4, D-10, D-20). The loader strips the
 * name for any row that is not `granted`, so a pending name cannot reach the
 * HTML even by mistake.
 */
export const clientSchema = z.object({
  id: nonEmpty,
  name: nonEmpty,
  /**
   * Only where evidenced. Several references have no evidenced sector, or sit
   * outside the four clusters; inventing one would be a claim, so the field is
   * optional and a row without it is not rendered.
   */
  sector: sectorId.optional(),
  /** Town, only where evidenced. */
  town: z.string().optional(),
  /**
   * A fact that must be verified before the town may be shown. Tetovo is the
   * one evidenced installation outside Gostivar, and BLUEPRINT-DELTA D-20
   * holds the wider service area until Kujto confirms it — so that row names
   * its gate here rather than relying on anyone remembering the rule.
   */
  townFact: z.string().optional(),
  modules: z.array(moduleId).default([]),
  permission: z.enum(["granted", "pending", "declined"]),
  /** Whether the sector was inferred rather than evidenced (D-10). */
  sectorInferred: z.boolean().default(false),
  source: nonEmpty,
});

export const clientsFileSchema = z.array(clientSchema).min(1);

export type Client = z.infer<typeof clientSchema>;

/** What a component is allowed to see. No name unless permission is granted. */
export type PublicClient = {
  id: string;
  name: string | null;
  sector: string;
  town: string | null;
  modules: string[];
};

/* -------------------------------------------------------------------------- */
/* tutorials/index.json                                                       */
/* -------------------------------------------------------------------------- */

export const tutorialSchema = z.object({
  /** YouTube id. */
  id: nonEmpty,
  title: nonEmpty,
  /** Interface/narration language of the video. */
  language: z.enum(["sq", "mk", "sq-mk"]),
  module: z.union([moduleId, z.literal("setup"), z.literal("system")]),
  duration: z.string().regex(/^\d{1,2}:\d{2}$/),
  date: z.string().regex(/^\d{4}-\d{2}(-\d{2})?$/),
  /**
   * Blueprint 14: every embed is spot-checked for real names before it is
   * listed. `listed: false` keeps a video out of the library until checked.
   */
  listed: z.boolean(),
  /** Why a video is not listed, for the handover log. */
  note: z.string().optional(),
  /** Nexys Web videos are tagged and hidden until the edition is confirmed. */
  edition: z.enum(["windows", "web"]).default("windows"),
});

export const tutorialsFileSchema = z.array(tutorialSchema).min(1);

export type Tutorial = z.infer<typeof tutorialSchema>;

/* -------------------------------------------------------------------------- */
/* support/channels.{locale}.json                                            */
/* -------------------------------------------------------------------------- */

export const supportSchema = z.object({
  intro: nonEmpty,
  /** Each channel gated on a fact; hours and remote tool are unknown. */
  channels: z.array(
    z.object({
      kind: z.enum(["phone", "whatsapp", "email", "hours", "remote", "facebook", "youtube"]),
      label: nonEmpty,
      value: nonEmpty,
      href: z.string().optional(),
      fact: nonEmpty,
    }),
  ),
  tutorialsIntro: nonEmpty,
  requestIntro: nonEmpty,
});

export type SupportContent = z.infer<typeof supportSchema>;

/* -------------------------------------------------------------------------- */
/* company/sulsys.{locale}.json                                              */
/* -------------------------------------------------------------------------- */

export const companySchema = z.object({
  name: nonEmpty,
  oneLiner: nonEmpty,
  /** Verified-facts paragraph(s) until Kujto supplies his own account. */
  story: z.array(nonEmpty).min(1).max(4),
  /** Ruled fact table; a fact without a value is not rendered. */
  facts: z.array(gatedRow),
  /** Services beyond Nexys — gated on D-08. */
  services: z.array(gatedFeature).default([]),
  location: z.object({
    heading: nonEmpty,
    address: z.array(nonEmpty).min(1),
    mapsUrl: nonEmpty,
    directionsLabel: nonEmpty,
  }),
  productsHeading: nonEmpty,
  /** Products list; v1 ships Nexys System only (Blueprint 07). */
  products: z
    .array(z.object({ name: nonEmpty, line: nonEmpty, target: z.literal("nexys") }))
    .min(1),
});

export type CompanyContent = z.infer<typeof companySchema>;

/** People render only with consent. No rows exist yet. */
export const peopleFileSchema = z.array(
  z.object({
    name: nonEmpty,
    role: nonEmpty,
    photo: z.string().optional(),
    consent: z.boolean(),
  }),
);

/* -------------------------------------------------------------------------- */
/* home.{locale}.json                                                        */
/* -------------------------------------------------------------------------- */

export const homeSchema = z.object({
  /**
   * 01 — the masthead. The company states what it is before the product is
   * named, because Sulsys is the company and Nexys is what it sells. `h1` is
   * the company's sentence, not the product's claim.
   */
  h1: nonEmpty,
  sub: nonEmpty,
  /**
   * What the company does, as single verbs — develops, installs, supports.
   * It sits beside the masthead sentence and answers "what kind of company is
   * this?" before the product is named. Three is the design; the schema allows
   * two to four so a locale can drop one rather than pad it.
   */
  works: z.array(nonEmpty).min(2).max(4),
  /**
   * The shelf under the masthead rule: what Sulsys builds. The flagship is
   * always Nexys System; `shelfNote` says what the product is in one line.
   */
  shelfLabel: nonEmpty,
  shelfNote: nonEmpty,
  /**
   * 02 — Nexys System introduced in its own band, under the company's roof.
   */
  productHeading: nonEmpty,
  productLead: nonEmpty,
  /** The record panel — the first "screen", honest about being a diagram. */
  panel: z.object({
    columns: z.array(nonEmpty).length(4),
    rows: z.array(z.array(nonEmpty).length(4)).min(5).max(7),
    total: z.array(nonEmpty).length(4),
    caption: nonEmpty,
  }),
  /** 05 — why a local system. Up to seven rows, each gated (D-13, D-16, D-17). */
  factRows: z.array(
    z.object({
      heading: nonEmpty,
      body: nonEmpty,
      fact: nonEmpty,
    }),
  ),
  /** 08 — support preview. */
  supportLead: nonEmpty,
  /** 09 — the Sulsys band that closes the page. */
  companyLead: nonEmpty,
  /** 10 — demo block heading. */
  demoHeading: nonEmpty,
  demoLead: nonEmpty,
});

export type HomeContent = z.infer<typeof homeSchema>;

/* -------------------------------------------------------------------------- */
/* trail.{locale}.json — the public record, by year                           */
/* -------------------------------------------------------------------------- */

/**
 * The continuity argument, and the only one the site is allowed to make about
 * the company's age.
 *
 * Sulsys's own "20 years" line is its claim, not ours, and it stays gated
 * (`softwareYears`) until Kujto confirms the wording. What *is* ours to show
 * is the dated public record: a Facebook page running since July 2012, a
 * tutorial channel that starts in July 2016 and keeps publishing, the 2022
 * hospitality series. Every row therefore names a fact id and is dropped if
 * that fact is not verified, exactly like every other claim on the site.
 *
 * `countsTutorials` asks the component to append the number of listed videos
 * from that year, counted from `tutorials/index.json` at build time rather
 * than typed into copy — a number that can drift out of truth is worse than
 * no number, and this one cannot.
 */
export const trailSchema = z.object({
  lead: nonEmpty,
  /** Where a visitor can go and check every row for themselves. */
  source: nonEmpty,
  entries: z
    .array(
      z.object({
        year: z.string().regex(/^\d{4}$/),
        label: nonEmpty,
        note: nonEmpty,
        fact: nonEmpty,
        countsTutorials: z.boolean().default(false),
      }),
    )
    .min(3),
});

export type TrailContent = z.infer<typeof trailSchema>;

/* -------------------------------------------------------------------------- */
/* glossary.json                                                              */
/* -------------------------------------------------------------------------- */

export const glossaryFileSchema = z.array(
  z.object({
    term: nonEmpty,
    sq: nonEmpty,
    mk: nonEmpty,
    en: nonEmpty,
    /** Terms that are never translated (Blueprint 10). */
    fixed: z.boolean().default(false),
  }),
);

export type GlossaryEntry = z.infer<typeof glossaryFileSchema>[number];

/* -------------------------------------------------------------------------- */
/* ui.{locale}.json                                                          */
/* -------------------------------------------------------------------------- */

/**
 * UI strings. Shape is enforced as a flat record of non-empty strings; the
 * content lint checks that every locale carries exactly the same key set, so a
 * string can never exist in one language only (Blueprint 17 rule 2).
 */
export const uiFileSchema = z.record(z.string(), nonEmpty);

export type UiStrings = Record<string, string>;

/* -------------------------------------------------------------------------- */
/* evidence.{locale}.json                                                     */
/* -------------------------------------------------------------------------- */

/**
 * The public-source evidence layer.
 *
 * Sulsys has a Facebook page running since 2012 and a YouTube channel since
 * 2016, and both are full of material that proves things this site otherwise
 * can only assert. V3.1 turns that archive into editorial citations attached
 * to the thing they are evidence *for* — a year-end closing walkthrough on the
 * Finance page, a restaurant workflow on Caffe — rather than a "follow us"
 * block nobody clicks.
 *
 * Three rules are enforced in content lint rather than remembered:
 *
 *   1. A YouTube item's `ref` must be an id that exists in
 *      tutorials/index.json, so a video's title, duration and date have one
 *      source of truth and an item cannot cite a video that is not in the
 *      library.
 *   2. A Facebook item's `ref` must appear verbatim in
 *      docs/research/PUBLIC-SOURCE-RESEARCH.md. No URL may be typed from
 *      memory; if it is not in the research, it does not exist.
 *   3. Every item names a fact, and an unverified fact removes the item
 *      entirely — so the archive cannot smuggle in a claim the register has
 *      not cleared.
 */
export const evidenceSchema = z.object({
  items: z.array(
    z.object({
      id: nonEmpty,
      source: z.enum(["youtube", "facebook"]),
      /** A YouTube id from the tutorial index, or a verified Facebook URL. */
      ref: nonEmpty,
      /** Which page it appears on. */
      placement: z.union([moduleId, z.literal("home"), z.literal("nexys"), z.literal("company")]),
      /** What it is evidence about; drives the kicker. */
      subject: z.union([moduleId, z.literal("system"), z.literal("company")]),
      /**
       * Facebook only — YouTube dates come from the tutorial index. Some
       * Facebook posts are only datable to a year from a relative timestamp
       * ("6 years ago"), and those are marked approximate rather than printed
       * as if they were exact.
       */
      date: z
        .string()
        .regex(/^\d{4}(-\d{2})?$/)
        .optional(),
      dateApprox: z.boolean().default(false),
      /** One sentence: what a reader will see, and why it matters. */
      lead: nonEmpty,
      /** The contextual action. Never "YouTube →". */
      cta: nonEmpty,
      /**
       * A self-hosted poster frame in public/screens/, by stem.
       *
       * YouTube's own thumbnails are not used: they are a third-party request
       * made before a reader asks for anything, and they can change without
       * anything here changing. A poster named here has been looked at and
       * cleared frame by frame — scripts/media/build-posters.mjs records what
       * was checked, and why most candidate frames were rejected. Without one,
       * the card draws its own plate.
       */
      poster: z.string().optional(),
      /** One item per page may lead; the rest are secondary. */
      featured: z.boolean().default(false),
      fact: nonEmpty,
    }),
  ),
});

export type EvidenceFile = z.infer<typeof evidenceSchema>;
export type EvidenceItem = EvidenceFile["items"][number];
