-- Migration 019: Procurement Intelligence Engine
-- Stock tracking, ABC classification, reorder signals, market intelligence
-- v0.2.0 — WS0c: Procurement Intelligence Foundation

-- ─────────────────────────────────────────────────────────────────────────────
-- 1. Stock Snapshots — time-series lagerbestand per vendor per product
-- ─────────────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS stock_snapshots (
  id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  transceiver_id UUID REFERENCES transceivers(id) ON DELETE CASCADE,
  vendor_id      UUID REFERENCES vendors(id) ON DELETE CASCADE,
  stock_level    TEXT CHECK (stock_level IN ('in_stock', 'out_of_stock', 'limited', 'unknown')) DEFAULT 'unknown',
  stock_quantity INT,                        -- exact quantity if vendor shows it
  incoming_quantity INT,                     -- "18 im Zulauf"
  incoming_eta   DATE,                       -- "verfügbar ab 15. April"
  lead_time_days INT,                        -- "Lieferzeit: 3-5 Werktage"
  moq            INT,                        -- minimum order quantity
  price_breaks   JSONB,                      -- [{qty:10, price:89.00}, {qty:50, price:74.00}]
  source_url     TEXT,
  crawler_confidence NUMERIC(3,2),           -- 0.00 – 1.00 (Crawler LLM confidence)
  scraped_at     TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_stock_transceiver ON stock_snapshots(transceiver_id, scraped_at DESC);
CREATE INDEX IF NOT EXISTS idx_stock_vendor       ON stock_snapshots(vendor_id, scraped_at DESC);
CREATE INDEX IF NOT EXISTS idx_stock_level        ON stock_snapshots(stock_level) WHERE stock_level != 'unknown';

-- ─────────────────────────────────────────────────────────────────────────────
-- 2. ABC Classification — computed turnover category
-- ─────────────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS abc_classification (
  transceiver_id  UUID PRIMARY KEY REFERENCES transceivers(id) ON DELETE CASCADE,
  abc_class       TEXT NOT NULL CHECK (abc_class IN ('A', 'B', 'C')),
  -- inputs
  obs_90d         INT DEFAULT 0,    -- price observations in last 90 days (proxy for market demand)
  compat_count    INT DEFAULT 0,    -- number of compatible switches (market breadth)
  vendor_count    INT DEFAULT 0,    -- number of vendors selling it (competition = demand signal)
  price_volatility NUMERIC(5,4),   -- STDDEV/AVG — high volatility = contested market
  -- derived signals
  demand_score    NUMERIC(5,2),    -- composite 0-100
  supply_risk     TEXT CHECK (supply_risk IN ('low', 'medium', 'high')),
  computed_at     TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_abc_class ON abc_classification(abc_class);

-- ─────────────────────────────────────────────────────────────────────────────
-- 3. Reorder Signals — computed buy/wait/hold/monitor recommendations
-- ─────────────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS reorder_signals (
  id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  transceiver_id UUID REFERENCES transceivers(id) ON DELETE CASCADE,
  signal         TEXT NOT NULL CHECK (signal IN ('buy_now', 'wait', 'hold', 'monitor')),
  signal_strength NUMERIC(3,2),   -- 0.00 – 1.00 (how strong the signal is)
  reasons        JSONB,           -- ["Stock declining at 3 vendors", "Lead time 16 weeks"]
  stock_trend    TEXT CHECK (stock_trend IN ('declining', 'stable', 'increasing', 'unknown')),
  price_trend    TEXT CHECK (price_trend IN ('falling', 'stable', 'rising', 'unknown')),
  lead_time_weeks INT,
  hype_phase     TEXT,            -- from hype_cycle data
  computed_at    TIMESTAMPTZ DEFAULT NOW(),
  expires_at     TIMESTAMPTZ DEFAULT NOW() + INTERVAL '24 hours'
);

CREATE INDEX IF NOT EXISTS idx_reorder_transceiver ON reorder_signals(transceiver_id, computed_at DESC);
CREATE INDEX IF NOT EXISTS idx_reorder_signal      ON reorder_signals(signal) WHERE expires_at > NOW();

-- ─────────────────────────────────────────────────────────────────────────────
-- 4. Product Lifecycle Events — EOL, new standards, CapEx peaks, trade shows
-- ─────────────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS product_lifecycle_events (
  id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  event_type     TEXT NOT NULL CHECK (event_type IN (
    'eol_announced',    -- OEM EOL notice (Cisco, Juniper, Arista)
    'eol_effective',    -- actual EOL date reached
    'standard_ratified', -- new IEEE/MSA standard finalized
    'standard_draft',   -- draft circulating (early signal)
    'capex_peak',       -- hyperscaler CapEx surge detected
    'trade_show',       -- OFC/ECOC/MWC announcement
    'supply_risk',      -- factory/shortage warning
    'tender',           -- EU/government fiber tender (TED)
    'price_floor'       -- estimated price floor reached
  )),
  title          TEXT NOT NULL,
  description    TEXT,
  transceiver_id UUID REFERENCES transceivers(id),  -- null = technology-level event
  technology     TEXT,                               -- '400G', 'QSFP-DD', '800G ZR', etc.
  effective_date DATE,                               -- when this event takes effect
  source_url     TEXT,
  source_name    TEXT,
  impact_level   TEXT CHECK (impact_level IN ('low', 'medium', 'high', 'critical')) DEFAULT 'medium',
  buy_signal     TEXT CHECK (buy_signal IN ('buy_now', 'wait', 'hold', 'monitor')),
  verified       BOOLEAN DEFAULT false,
  created_at     TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_lifecycle_type       ON product_lifecycle_events(event_type, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_lifecycle_technology ON product_lifecycle_events(technology) WHERE technology IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_lifecycle_signal     ON product_lifecycle_events(buy_signal) WHERE buy_signal IS NOT NULL;

-- ─────────────────────────────────────────────────────────────────────────────
-- 5. Market Intelligence — hyperscaler CapEx, OFC/ECOC, standards, tenders
-- ─────────────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS market_intelligence (
  id                    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  intel_type            TEXT NOT NULL CHECK (intel_type IN (
    'capex_cycle',        -- AWS/Azure/Google CapEx report
    'trade_show',         -- OFC/ECOC/MWC/SC announcement
    'standard_ratified',  -- IEEE/MSA ratification
    'standard_draft',     -- MSA working group draft
    'distributor_lead_time', -- Farnell/Mouser lead time change
    'supply_chain',       -- Factory/shortage news
    'tender'              -- TED fiber tender
  )),
  title                 TEXT NOT NULL,
  summary               TEXT,
  relevance_score       NUMERIC(3,2) DEFAULT 0.5,  -- 0-1, LLM-assessed relevance
  technologies          TEXT[],                     -- ['400G', 'QSFP-DD', 'ZR']
  buy_signal_implication TEXT CHECK (buy_signal_implication IN ('buy_now', 'wait', 'hold', 'monitor', 'none')),
  impact_horizon_months INT,                        -- how many months until this matters
  source_url            TEXT,
  source_name           TEXT NOT NULL,
  published_at          TIMESTAMPTZ,
  created_at            TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_intel_type        ON market_intelligence(intel_type, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_intel_technologies ON market_intelligence USING gin(technologies);
CREATE INDEX IF NOT EXISTS idx_intel_signal      ON market_intelligence(buy_signal_implication);

-- ─────────────────────────────────────────────────────────────────────────────
-- 6. Crawler LLM Scrape Log — audit trail for Crawler LLM results
-- ─────────────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS crawler_llm_log (
  id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  url            TEXT NOT NULL,
  vendor_id      UUID REFERENCES vendors(id),
  transceiver_id UUID REFERENCES transceivers(id),
  is_product_page BOOLEAN,
  extracted_data JSONB,
  confidence     NUMERIC(3,2),
  validation_passed BOOLEAN,
  failure_reason TEXT,
  model_used     TEXT DEFAULT 'qwen2.5:14b',
  scraped_at     TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_llm_log_url    ON crawler_llm_log(url, scraped_at DESC);
CREATE INDEX IF NOT EXISTS idx_llm_log_vendor ON crawler_llm_log(vendor_id, scraped_at DESC);

-- ─────────────────────────────────────────────────────────────────────────────
-- 7. Useful views
-- ─────────────────────────────────────────────────────────────────────────────

-- Latest stock per product per vendor
CREATE OR REPLACE VIEW v_stock_current AS
SELECT DISTINCT ON (ss.transceiver_id, ss.vendor_id)
  ss.*,
  t.part_number, t.standard_name, t.form_factor, t.speed_gbps,
  v.name AS vendor_name, v.slug AS vendor_slug
FROM stock_snapshots ss
JOIN transceivers t ON ss.transceiver_id = t.id
JOIN vendors v ON ss.vendor_id = v.id
ORDER BY ss.transceiver_id, ss.vendor_id, ss.scraped_at DESC;

-- Active reorder signals (not expired)
CREATE OR REPLACE VIEW v_reorder_signals_active AS
SELECT rs.*,
  t.part_number, t.standard_name, t.form_factor, t.speed_gbps, t.reach_label,
  ac.abc_class
FROM reorder_signals rs
JOIN transceivers t ON rs.transceiver_id = t.id
LEFT JOIN abc_classification ac ON ac.transceiver_id = t.id
WHERE rs.expires_at > NOW()
  AND rs.computed_at = (
    SELECT MAX(computed_at) FROM reorder_signals r2 WHERE r2.transceiver_id = rs.transceiver_id
  )
ORDER BY rs.signal_strength DESC;

-- Stock trend (is it declining at vendors?)
CREATE OR REPLACE VIEW v_stock_trend AS
SELECT
  transceiver_id,
  vendor_id,
  COUNT(*) AS snapshot_count,
  -- Compare recent vs older snapshots
  COUNT(*) FILTER (WHERE stock_level = 'out_of_stock' AND scraped_at > NOW() - INTERVAL '7 days') AS oos_recent,
  COUNT(*) FILTER (WHERE stock_level = 'in_stock'     AND scraped_at > NOW() - INTERVAL '7 days') AS in_stock_recent,
  COUNT(*) FILTER (WHERE stock_level = 'out_of_stock' AND scraped_at BETWEEN NOW() - INTERVAL '30 days' AND NOW() - INTERVAL '7 days') AS oos_older,
  CASE
    WHEN COUNT(*) FILTER (WHERE stock_level = 'out_of_stock' AND scraped_at > NOW() - INTERVAL '7 days') >
         COUNT(*) FILTER (WHERE stock_level = 'out_of_stock' AND scraped_at BETWEEN NOW() - INTERVAL '30 days' AND NOW() - INTERVAL '7 days')
    THEN 'declining'
    WHEN COUNT(*) FILTER (WHERE stock_level = 'in_stock' AND scraped_at > NOW() - INTERVAL '7 days') > 2 THEN 'stable'
    ELSE 'unknown'
  END AS trend
FROM stock_snapshots
WHERE scraped_at > NOW() - INTERVAL '30 days'
GROUP BY transceiver_id, vendor_id;

-- ─────────────────────────────────────────────────────────────────────────────
-- 8. Seed: Known market intelligence events (static knowledge base)
-- ─────────────────────────────────────────────────────────────────────────────
INSERT INTO market_intelligence (intel_type, title, summary, relevance_score, technologies, buy_signal_implication, impact_horizon_months, source_name, published_at) VALUES
(
  'trade_show',
  'OFC 2026 — Key 800G ZR and Co-Packaged Optics Announcements',
  'OFC 2026 highlighted accelerated 800G ZR deployment timelines and first Co-Packaged Optics (CPO) demos from Broadcom and Intel. CPO replaces pluggable modules in 4-6 years for hyperscaler intra-DC. Short term: 400G ZR+ and 800G QSFP-DD demand surge expected in 2026-2027.',
  0.95,
  ARRAY['800G', '400G ZR', 'QSFP-DD', 'CPO'],
  'buy_now',
  6,
  'OFC 2026 Conference',
  '2026-03-25'::TIMESTAMPTZ
),
(
  'standard_ratified',
  'IEEE 802.3df — 100G, 200G, 400G Ethernet over single-mode fiber',
  '802.3df ratified December 2024. Defines 100GBASE-DR, 200GBASE-DR4, 400GBASE-DR4 with PAM4 modulation. Vendors shipping compliant optics in H1 2026. Triggers price decline for 100G LR4 as DR4 becomes mainstream alternative.',
  0.88,
  ARRAY['100G', '200G', '400G', 'DR4', 'PAM4'],
  'wait',
  3,
  'IEEE 802.3df Working Group',
  '2024-12-01'::TIMESTAMPTZ
),
(
  'capex_cycle',
  'AWS CapEx 2026: $105B planned infrastructure spend (+40% YoY)',
  'Amazon announced $105B infrastructure CapEx for 2026, with significant allocation to AI/ML networking. Q1/Q2 typically slower, Q3/Q4 peak deployment. Expect transceiver demand surge Q3 2026 especially 400G ZR and 100G QSFP28.',
  0.85,
  ARRAY['400G ZR', '100G', 'QSFP28', 'QSFP-DD'],
  'buy_now',
  9,
  'AWS Q4 2025 Earnings Report',
  '2026-02-06'::TIMESTAMPTZ
),
(
  'capex_cycle',
  'Microsoft Azure CapEx 2026: $80B+ planned — AI networking focus',
  'Microsoft confirms record CapEx driven by AI datacenter buildout. Azure networking upgrades prioritizing 400G+ spine/leaf. Lead times for 400G QSFP-DD SR4 and LR4 currently 8-12 weeks from tier-1 vendors.',
  0.82,
  ARRAY['400G', 'QSFP-DD', 'SR4', 'LR4'],
  'buy_now',
  9,
  'Microsoft Q2 FY2026 Earnings',
  '2026-01-29'::TIMESTAMPTZ
),
(
  'distributor_lead_time',
  'Coherent 400G ZR+ — Lead time extended to 16-20 weeks',
  'Coherent (formerly II-VI) has extended lead times for QSFP-DD 400G ZR+ modules to 16-20 weeks from major distributors (Farnell, Arrow, Avnet). Cause: wafer fab capacity constrained by AI optics demand. Expected normalization Q4 2026.',
  0.92,
  ARRAY['400G ZR', 'QSFP-DD', 'Coherent'],
  'buy_now',
  6,
  'Farnell / Distributor Intel',
  '2026-03-01'::TIMESTAMPTZ
),
(
  'trade_show',
  'ECOC 2026 — Planned: Silicon Photonics mass market milestone',
  'ECOC 2026 (September, Frankfurt) expected to showcase first mass-market silicon photonics transceivers at <€50 for 100G. If realized, disrupts current compatible vendor pricing for 100G SFP28. Monitor closely for 100G category.',
  0.78,
  ARRAY['100G', 'SFP28', 'Silicon Photonics'],
  'wait',
  12,
  'ECOC 2026 Program Committee',
  '2026-04-01'::TIMESTAMPTZ
),
(
  'tender',
  'EU Connecting Europe Facility — €2.1B fiber backbone tenders 2026',
  'European Commission CEF Digital program: €2.1B in fiber backbone tenders across DE, FR, PL, SE in 2026. Each tender = 6-18 month deployment window. Triggers DWDM + ROADM + coherent transceiver demand (100G/400G ZR). TED database: TED-OJ.',
  0.75,
  ARRAY['DWDM', '100G', '400G ZR', 'Coherent', 'ROADM'],
  'monitor',
  18,
  'EU TED / Connecting Europe Facility',
  '2026-01-15'::TIMESTAMPTZ
)
ON CONFLICT DO NOTHING;

-- ─────────────────────────────────────────────────────────────────────────────
-- 9. Seed: Known lifecycle events
-- ─────────────────────────────────────────────────────────────────────────────
INSERT INTO product_lifecycle_events (event_type, title, description, technology, effective_date, source_name, impact_level, buy_signal) VALUES
(
  'eol_announced',
  'Cisco SFP-10G-LR — EOL announced, EOS 2027-06-30',
  'Cisco Product Bulletin: SFP-10G-LR (CS-SFPHLX10G-LR) enters End of Sale 2026-06-30, End of Support 2027-06-30. Customers must migrate to SFP-10G-LR-S or compatible alternatives. Hortungs-Rush expected Q1-Q2 2026.',
  '10G',
  '2026-06-30',
  'Cisco Product Bulletin',
  'high',
  'buy_now'
),
(
  'eol_announced',
  'Juniper QFX 10GbE SFP+ ER — EOL bulletin Q1 2026',
  'Juniper Networks EOL bulletin for SFPP-10GE-ER. End of Engineering 2026-06-01. Last time order date 2026-09-01. Customers should evaluate EX-SFP-10GE-ER-S alternatives.',
  '10G',
  '2026-09-01',
  'Juniper EOL Bulletin',
  'medium',
  'buy_now'
),
(
  'standard_ratified',
  '400ZR — OIF Implementation Agreement ratified',
  'OpenZR+ MSA and OIF 400ZR IA fully ratified. Multi-vendor interoperability confirmed at Interop events. Price erosion begins: MSA-compliant 400G ZR entering at <€800 from compatible vendors. OEM premium shrinking.',
  '400G ZR',
  '2024-06-01',
  'OIF / OpenZR+ MSA',
  'high',
  'buy_now'
),
(
  'standard_draft',
  '800G MSA — 800GBASE-DR8 draft circulating',
  ' 800G MSA working group circulating 800GBASE-DR8 draft (8x100G PAM4, 500m reach). Expected ratification Q3 2026. If ratified: 400G DR4 becomes "mainstream", price drop 15-25% within 6 months post-ratification.',
  '800G',
  '2026-09-01',
  '800G MSA Working Group',
  'medium',
  'hold'
)
ON CONFLICT DO NOTHING;

-- ─────────────────────────────────────────────────────────────────────────────
-- Done
-- ─────────────────────────────────────────────────────────────────────────────
SELECT
  (SELECT COUNT(*) FROM stock_snapshots) AS stock_snapshots,
  (SELECT COUNT(*) FROM abc_classification) AS abc_entries,
  (SELECT COUNT(*) FROM reorder_signals) AS reorder_signals,
  (SELECT COUNT(*) FROM product_lifecycle_events) AS lifecycle_events,
  (SELECT COUNT(*) FROM market_intelligence) AS market_intel_entries,
  (SELECT COUNT(*) FROM crawler_llm_log) AS crawler_log_entries;
