import type Database from "better-sqlite3";
import { z } from "zod";
import type { PurchasePlan } from "../../shared/contract.js";
import { OfferSchema } from "./types.js";
import type { PurchasePlanStorage } from "./types.js";

const nullableText = z.string().min(1).nullable();

const RefreshAttemptSchema = z.object({
  attemptedAt: z.number().int().nonnegative(),
  outcome: z.enum(["up_to_date", "changed", "not_found", "could_not_check", "human_check_required"]),
  reason: nullableText,
});

const OfferChangeSchema = z.object({
  detectedAt: z.number().int().nonnegative(),
  beforePriceEur: z.number().finite().positive(),
  currentPriceEur: z.number().finite().positive(),
  beforeInStock: z.boolean().nullable(),
  currentInStock: z.boolean().nullable(),
  beforeAvailability: nullableText,
  currentAvailability: nullableText,
});

const PlanCandidateSchema = z.object({
  id: z.string().min(1).max(200),
  storeId: z.string().min(1).max(40),
  storeName: z.string().min(1).max(200),
  offer: OfferSchema.extend({ sourceRef: z.string().min(1).max(200).nullable() }),
  addedAt: z.number().int().nonnegative(),
  observedAt: z.number().int().nonnegative().nullable(),
  lastAttempt: RefreshAttemptSchema.nullable(),
  unreviewedChange: OfferChangeSchema.nullable(),
});

const PartRequirementSchema = z.object({
  id: z.string().min(1).max(200),
  partKey: z.string().min(1).max(300),
  brand: nullableText,
  partNumber: z.string().min(1).max(120),
  title: z.string().min(1).max(500),
  imageUrl: z.string().url().nullable(),
  quantity: z.number().int().min(1).max(10_000),
  chosenCandidateId: z.string().min(1).max(200),
  candidates: z.array(PlanCandidateSchema).min(1).max(100),
});

export const PurchasePlanSchema = z.object({
  schemaVersion: z.literal(1),
  storeSort: z.enum(["lines", "subtotal", "alphabetical"]),
  requirements: z.array(PartRequirementSchema).max(1_000),
}).superRefine((plan, ctx) => {
  const requirementIds = new Set<string>();
  const partKeys = new Set<string>();
  for (const [index, requirement] of plan.requirements.entries()) {
    if (requirementIds.has(requirement.id) || partKeys.has(requirement.partKey)) {
      ctx.addIssue({ code: "custom", path: ["requirements", index], message: "duplicate requirement" });
    }
    requirementIds.add(requirement.id);
    partKeys.add(requirement.partKey);
    const candidateIds = new Set(requirement.candidates.map((candidate) => candidate.id));
    if (candidateIds.size !== requirement.candidates.length || !candidateIds.has(requirement.chosenCandidateId)) {
      ctx.addIssue({ code: "custom", path: ["requirements", index, "candidates"], message: "invalid candidate selection" });
    }
  }
}) satisfies z.ZodType<PurchasePlan>;

/** Persistent single-user purchase plan. The singleton key leaves room for accounts later. */
export class PurchasePlanRepository implements PurchasePlanStorage {
  constructor(private db: Database.Database) {
    this.db.exec(`
      CREATE TABLE IF NOT EXISTS purchase_plan (
        id          INTEGER PRIMARY KEY CHECK (id = 1),
        updated_at  INTEGER NOT NULL,
        document    TEXT NOT NULL
      )
    `);
  }

  get(): { plan: PurchasePlan; updatedAt: number } | null {
    const row = this.db.prepare("SELECT updated_at, document FROM purchase_plan WHERE id = 1")
      .get() as { updated_at: number; document: string } | undefined;
    if (!row) return null;
    let value: unknown;
    try { value = JSON.parse(row.document); } catch { return null; }
    const parsed = PurchasePlanSchema.safeParse(value);
    return parsed.success ? { plan: parsed.data, updatedAt: row.updated_at } : null;
  }

  set(plan: PurchasePlan, updatedAt = Date.now()): number {
    this.db.prepare(
      `INSERT INTO purchase_plan (id, updated_at, document) VALUES (1, ?, ?)
       ON CONFLICT(id) DO UPDATE SET updated_at = excluded.updated_at, document = excluded.document`,
    ).run(updatedAt, JSON.stringify(plan));
    return updatedAt;
  }
}
