import { Database } from "bun:sqlite";
import { mkdirSync as fsMkdirSync } from "node:fs";
import { dirname } from "node:path";

import type { CheckFailure, ResolvedTargetBase, StoredCheckResult, StoredTarget } from "./types";

import { checkerRegistry } from "./runner";

const CREATE_TARGETS_TABLE = `
CREATE TABLE IF NOT EXISTS targets (
  id TEXT PRIMARY KEY,
  name TEXT,
  description TEXT,
  type TEXT NOT NULL,
  target TEXT NOT NULL,
  config TEXT NOT NULL DEFAULT '{}',
  interval_ms INTEGER NOT NULL,
  timeout_ms INTEGER NOT NULL,
  expect TEXT,
  grp TEXT NOT NULL DEFAULT 'default',
  active INTEGER NOT NULL DEFAULT 1
)
`;

const CREATE_CHECK_RESULTS_TABLE = `
CREATE TABLE IF NOT EXISTS check_results (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  target_id TEXT NOT NULL,
  timestamp TEXT NOT NULL,
  matched INTEGER NOT NULL,
  duration_ms REAL,
  observation TEXT,
  failure TEXT,
  FOREIGN KEY (target_id) REFERENCES targets(id) ON DELETE RESTRICT
)
`;

const CREATE_INDEX = `
CREATE INDEX IF NOT EXISTS idx_check_results_target_timestamp
ON check_results (target_id, timestamp)
`;

export class ProbeStore {
  private closed = false;
  private db: Database;

  constructor(dbPath: string) {
    ensureDir(dirname(dbPath));
    this.db = new Database(dbPath, { create: true });
    this.db.run("PRAGMA journal_mode = WAL");
    this.db.run("PRAGMA foreign_keys = ON");
    this.db.run(CREATE_TARGETS_TABLE);
    this.db.run(CREATE_CHECK_RESULTS_TABLE);
    this.db.run(CREATE_INDEX);
  }

  close(): void {
    this.closed = true;
    this.db.close();
  }

  deleteTargetRaw(id: string): void {
    if (this.closed) return;
    this.db.run("DELETE FROM targets WHERE id = ?", [id]);
  }

  getAllRecentSamples(
    limit: number,
  ): Map<string, Array<{ duration_ms: null | number; matched: number; timestamp: string }>> {
    if (this.closed) return new Map();

    const rows = this.db
      .query(
        `SELECT target_id, timestamp, duration_ms, matched
         FROM (
           SELECT
             target_id,
             timestamp,
             duration_ms,
             matched,
             ROW_NUMBER() OVER (PARTITION BY target_id ORDER BY timestamp DESC) as row_num
           FROM check_results
           WHERE target_id IN (SELECT id FROM targets WHERE active = 1)
         )
         WHERE row_num <= ?
         ORDER BY target_id, timestamp DESC`,
      )
      .all(limit) as Array<{
      duration_ms: null | number;
      matched: number;
      target_id: string;
      timestamp: string;
    }>;

    const result = new Map<string, Array<{ duration_ms: null | number; matched: number; timestamp: string }>>();
    for (const row of rows) {
      const samples = result.get(row.target_id) ?? [];
      samples.push({ duration_ms: row.duration_ms, matched: row.matched, timestamp: row.timestamp });
      result.set(row.target_id, samples);
    }
    return result;
  }

  getAllTargetWindowStats(
    from: string,
    to: string,
  ): Map<string, { availability: number; downChecks: number; totalChecks: number; upChecks: number }> {
    if (this.closed) return new Map();

    const rows = this.db
      .query(
        `SELECT target_id, COUNT(*) as totalChecks,
          COALESCE(SUM(CASE WHEN matched = 1 THEN 1 ELSE 0 END), 0) as upChecks,
          COALESCE(SUM(CASE WHEN matched = 0 THEN 1 ELSE 0 END), 0) as downChecks
         FROM check_results
         WHERE timestamp >= ? AND timestamp <= ?
           AND target_id IN (SELECT id FROM targets WHERE active = 1)
         GROUP BY target_id`,
      )
      .all(from, to) as Array<{ downChecks: number; target_id: string; totalChecks: number; upChecks: number }>;

    const result = new Map<
      string,
      { availability: number; downChecks: number; totalChecks: number; upChecks: number }
    >();
    for (const row of rows) {
      const availability = row.totalChecks > 0 ? Math.round((row.upChecks / row.totalChecks) * 100 * 100) / 100 : 0;
      result.set(row.target_id, {
        availability,
        downChecks: row.downChecks,
        totalChecks: row.totalChecks,
        upChecks: row.upChecks,
      });
    }
    return result;
  }

  getDashboardIncidentStates(
    from: string,
    to: string,
  ): Array<{ matched: number; target_id: string; timestamp: string }> {
    if (this.closed) return [];

    return this.db
      .query(
        `SELECT target_id, timestamp, matched
         FROM check_results
         WHERE timestamp >= ? AND timestamp <= ?
           AND target_id IN (SELECT id FROM targets WHERE active = 1)
         ORDER BY target_id ASC, timestamp ASC`,
      )
      .all(from, to) as Array<{ matched: number; target_id: string; timestamp: string }>;
  }

  getHistory(
    targetId: string,
    from: string,
    to: string,
    page = 1,
    pageSize = 20,
  ): { items: StoredCheckResult[]; page: number; pageSize: number; total: number } {
    const countRow = this.db
      .query("SELECT COUNT(*) as total FROM check_results WHERE target_id = ? AND timestamp >= ? AND timestamp <= ?")
      .get(targetId, from, to) as { total: number };

    const offset = (page - 1) * pageSize;
    const items = this.db
      .query(
        "SELECT * FROM check_results WHERE target_id = ? AND timestamp >= ? AND timestamp <= ? ORDER BY timestamp DESC LIMIT ? OFFSET ?",
      )
      .all(targetId, from, to, pageSize, offset) as StoredCheckResult[];

    return { items, page, pageSize, total: countRow.total };
  }

  getLatestCheck(targetId: string): null | StoredCheckResult {
    return this.db
      .query("SELECT * FROM check_results WHERE target_id = ? ORDER BY timestamp DESC LIMIT 1")
      .get(targetId) as null | StoredCheckResult;
  }

  getLatestChecksMap(): Map<string, StoredCheckResult> {
    const rows = this.db
      .query(
        `SELECT cr.* FROM check_results cr
         INNER JOIN (
           SELECT target_id, MAX(timestamp) as max_ts
           FROM check_results
           WHERE target_id IN (SELECT id FROM targets WHERE active = 1)
           GROUP BY target_id
         ) latest ON cr.target_id = latest.target_id AND cr.timestamp = latest.max_ts`,
      )
      .all() as StoredCheckResult[];
    return new Map(rows.map((r) => [r.target_id, r]));
  }

  getRecentSamples(
    targetId: string,
    limit: number,
  ): Array<{ duration_ms: null | number; matched: number; timestamp: string }> {
    return this.db
      .query(
        "SELECT timestamp, duration_ms, matched FROM check_results WHERE target_id = ? ORDER BY timestamp DESC LIMIT ?",
      )
      .all(targetId, limit) as Array<{
      duration_ms: null | number;
      matched: number;
      timestamp: string;
    }>;
  }

  getTargetActive(id: string): null | number {
    if (this.closed) return null;
    const row = this.db.query("SELECT active FROM targets WHERE id = ?").get(id) as null | { active: number };
    return row?.active ?? null;
  }

  getTargetById(id: string): null | StoredTarget {
    if (this.closed) return null;
    return this.db.query("SELECT * FROM targets WHERE id = ? AND active = 1").get(id) as null | StoredTarget;
  }

  getTargetCheckpoints(
    targetId: string,
    from: string,
    to: string,
  ): Array<{ duration_ms: null | number; matched: number; timestamp: string }> {
    if (this.closed) return [];

    return this.db
      .query(
        `SELECT timestamp, matched, duration_ms
         FROM check_results
         WHERE target_id = ? AND timestamp >= ? AND timestamp <= ?
         ORDER BY timestamp ASC`,
      )
      .all(targetId, from, to) as Array<{ duration_ms: null | number; matched: number; timestamp: string }>;
  }

  getTargetDurations(targetId: string, from: string, to: string): number[] {
    if (this.closed) return [];

    const rows = this.db
      .query(
        `SELECT duration_ms
         FROM check_results
         WHERE target_id = ? AND timestamp >= ? AND timestamp <= ? AND matched = 1 AND duration_ms IS NOT NULL
         ORDER BY duration_ms ASC`,
      )
      .all(targetId, from, to) as Array<{ duration_ms: number }>;

    return rows.map((row) => row.duration_ms);
  }

  getTargets(): StoredTarget[] {
    if (this.closed) return [];
    return this.db
      .query("SELECT * FROM targets WHERE active = 1 ORDER BY CASE WHEN grp='default' THEN 0 ELSE 1 END, id")
      .all() as StoredTarget[];
  }

  getTargetWindowStats(
    targetId: string,
    from: string,
    to: string,
  ): {
    availability: number;
    downChecks: number;
    totalChecks: number;
    upChecks: number;
  } {
    if (this.closed) return { availability: 0, downChecks: 0, totalChecks: 0, upChecks: 0 };

    const row = this.db
      .query(
        `SELECT
          COUNT(*) as totalChecks,
          COALESCE(SUM(CASE WHEN matched = 1 THEN 1 ELSE 0 END), 0) as upChecks,
          COALESCE(SUM(CASE WHEN matched = 0 THEN 1 ELSE 0 END), 0) as downChecks
        FROM check_results
        WHERE target_id = ? AND timestamp >= ? AND timestamp <= ?`,
      )
      .get(targetId, from, to) as { downChecks: number; totalChecks: number; upChecks: number };

    const totalChecks = row.totalChecks;
    const availability = totalChecks > 0 ? (row.upChecks / totalChecks) * 100 : 0;

    return {
      availability: Math.round(availability * 100) / 100,
      downChecks: row.downChecks,
      totalChecks,
      upChecks: row.upChecks,
    };
  }

  hasTargetRow(id: string): boolean {
    if (this.closed) return false;
    const row = this.db.query("SELECT 1 FROM targets WHERE id = ?").get(id) as null | { "1": number };
    return row !== null;
  }

  insertCheckResult(result: {
    durationMs: null | number;
    failure: CheckFailure | null;
    matched: boolean;
    observation: null | Record<string, unknown>;
    targetId: string;
    timestamp: string;
  }): void {
    if (this.closed) return;
    this.db
      .query(
        "INSERT INTO check_results (target_id, timestamp, matched, duration_ms, observation, failure) VALUES (?, ?, ?, ?, ?, ?)",
      )
      .run(
        result.targetId,
        result.timestamp,
        result.matched ? 1 : 0,
        result.durationMs,
        result.observation ? JSON.stringify(result.observation) : null,
        result.failure ? JSON.stringify(result.failure) : null,
      );
  }

  prune(retentionMs: number): number {
    if (this.closed) return 0;
    const cutoff = new Date(Date.now() - retentionMs).toISOString();
    const result = this.db.run("DELETE FROM check_results WHERE timestamp < ?", [cutoff]);
    this.db.run(
      "DELETE FROM targets WHERE active = 0 AND NOT EXISTS (SELECT 1 FROM check_results WHERE check_results.target_id = targets.id)",
    );
    return result.changes;
  }

  syncTargets(targets: ResolvedTargetBase[]): void {
    if (this.closed) return;
    const existingRows = this.db.query("SELECT id FROM targets").all() as Array<{ id: string }>;
    const existingIds = new Set(existingRows.map((r) => r.id));
    const configIds = new Set(targets.map((t) => t.id));

    const insertStmt = this.db.prepare(
      "INSERT INTO targets (id, name, description, type, target, config, interval_ms, timeout_ms, expect, grp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
    );
    const updateStmt = this.db.prepare(
      "UPDATE targets SET name = ?, description = ?, type = ?, target = ?, config = ?, interval_ms = ?, timeout_ms = ?, expect = ?, grp = ?, active = 1 WHERE id = ?",
    );
    const deactivateStmt = this.db.prepare("UPDATE targets SET active = 0 WHERE id = ? AND active = 1");

    const tx = this.db.transaction(() => {
      for (const t of targets) {
        const type = t.type;
        const serialized = checkerRegistry.get(t.type).serialize(t);
        const target = serialized.target;
        const config = serialized.config;
        const expect = t.rawExpect ? JSON.stringify(t.rawExpect) : null;

        if (existingIds.has(t.id)) {
          updateStmt.run(t.name, t.description, type, target, config, t.intervalMs, t.timeoutMs, expect, t.group, t.id);
        } else {
          insertStmt.run(t.id, t.name, t.description, type, target, config, t.intervalMs, t.timeoutMs, expect, t.group);
        }
      }

      for (const id of existingIds) {
        if (!configIds.has(id)) {
          deactivateStmt.run(id);
        }
      }
    });

    tx();
  }
}

function ensureDir(dir: string): void {
  try {
    fsMkdirSync(dir, { recursive: true });
  } catch {
    // already exists
  }
}
