import * as SQLite from 'expo-sqlite';
import * as FileSystem from 'expo-file-system/legacy';
import type { MeasurementFrame, MeasurementMeta, SetupConfig, GateConfig } from '../protocol/types';
import { AMP_GAIN } from '../protocol/constants';
import * as BinLoader from './BinLoader';
import * as TemBundle from './TemBundle';

const DATA_DIR = (FileSystem.documentDirectory ?? '') + 'tem_data/';

// ── Public types ────────────────────────────────────────────────────────────

export interface ProjectInfo {
  projectId:  string;
  name:       string;
  createdAt:  number;
  lineCount:  number;
}

export interface SessionInfo {
  sessionId:  string;
  createdAt:  number;
  frameCount: number;
  projectId:  string | null;
}

export interface PersistedFrame {
  frameId:   number;
  sessionId: string;
  timestamp: number;
  accNum:    number;
  gain:      number;
  binPath:   string | null;
  meta:      MeasurementMeta;
}

// ── DB init ─────────────────────────────────────────────────────────────────

let _db: SQLite.SQLiteDatabase | null = null;

async function getDb(): Promise<SQLite.SQLiteDatabase> {
  if (!_db) {
    _db = await SQLite.openDatabaseAsync('tem.db');
    await _db.execAsync(`
      PRAGMA journal_mode = WAL;

      CREATE TABLE IF NOT EXISTS projects (
        project_id  TEXT    PRIMARY KEY,
        name        TEXT    NOT NULL,
        created_at  INTEGER NOT NULL
      );

      CREATE TABLE IF NOT EXISTS sessions (
        session_id  TEXT    PRIMARY KEY,
        created_at  INTEGER NOT NULL,
        frame_count INTEGER NOT NULL DEFAULT 0,
        project_id  TEXT    REFERENCES projects(project_id) ON DELETE SET NULL
      );

      CREATE TABLE IF NOT EXISTS frames (
        frame_id   INTEGER NOT NULL,
        session_id TEXT    NOT NULL,
        ts         INTEGER,
        utc        INTEGER, lon REAL, lat REAL, alt REAL,
        ch_num     INTEGER, acc_num INTEGER, gain REAL,
        gps_st     INTEGER, sd_st   INTEGER, amp_ratio INTEGER,
        current    INTEGER, temp    INTEGER, batt      INTEGER,
        roll REAL,  pitch REAL, yaw REAL, src_mode INTEGER,
        bin_path   TEXT,
        PRIMARY KEY (frame_id, session_id)
      );

      CREATE INDEX IF NOT EXISTS idx_fs ON frames(session_id);
    `);
    await runMigrations(_db);
  }
  return _db;
}

async function runMigrations(db: SQLite.SQLiteDatabase) {
  const row = await db.getFirstAsync<{ user_version: number }>('PRAGMA user_version');
  let version = row?.user_version ?? 0;

  if (version < 1) {
    try { await db.runAsync('ALTER TABLE sessions ADD COLUMN project_id TEXT'); } catch {}
    await db.runAsync('PRAGMA user_version = 1');
    version = 1;
  }
  if (version < 2) {
    const orphans = await db.getAllAsync<{ session_id: string }>(
      'SELECT session_id FROM sessions WHERE project_id IS NULL',
    );
    if (orphans.length > 0) {
      const defId = generateProjectId();
      await db.runAsync(
        'INSERT INTO projects (project_id, name, created_at) VALUES (?,?,?)',
        [defId, '默认工程', Date.now()],
      );
      await db.runAsync('UPDATE sessions SET project_id = ? WHERE project_id IS NULL', [defId]);
    }
    await db.runAsync('PRAGMA user_version = 2');
    version = 2;
  }
}

async function ensureDataDir() {
  const info = await FileSystem.getInfoAsync(DATA_DIR);
  if (!info.exists) await FileSystem.makeDirectoryAsync(DATA_DIR, { intermediates: true });
}

export async function initStorage(): Promise<void> {
  await getDb();
  await ensureDataDir();
}

export async function hasAnyProject(): Promise<boolean> {
  const d = await getDb();
  const row = await d.getFirstAsync<{ cnt: number }>('SELECT COUNT(*) AS cnt FROM projects');
  return (row?.cnt ?? 0) > 0;
}

export async function getFirstProject(): Promise<ProjectInfo | null> {
  const d = await getDb();
  const r = await d.getFirstAsync<{ project_id: string; name: string; created_at: number }>(
    'SELECT project_id, name, created_at FROM projects ORDER BY created_at DESC LIMIT 1',
  );
  if (!r) return null;
  return { projectId: r.project_id, name: r.name, createdAt: r.created_at, lineCount: 0 };
}

// ── Project CRUD ────────────────────────────────────────────────────────────

function generateProjectId(): string {
  const now = new Date();
  const pad = (n: number, d = 2) => String(n).padStart(d, '0');
  const rand = Math.random().toString(36).slice(2, 6);
  return (
    `P${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` +
    `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}` +
    `${pad(now.getMilliseconds(), 3)}_${rand}`
  );
}

function sanitizeName(name: string): string {
  return name.trim().replace(/[\/\\:*?"<>|.\x00]/g, '_').slice(0, 50);
}

export async function createProject(name: string): Promise<string> {
  const d = await getDb();
  const id = generateProjectId();
  await d.runAsync(
    'INSERT INTO projects (project_id, name, created_at) VALUES (?,?,?)',
    [id, sanitizeName(name), Date.now()],
  );
  return id;
}

export async function listProjects(): Promise<ProjectInfo[]> {
  const d = await getDb();
  const rows = await d.getAllAsync<{
    project_id: string; name: string; created_at: number; line_count: number;
  }>(
    `SELECT p.project_id, p.name, p.created_at,
            COUNT(s.session_id) AS line_count
     FROM projects p
     LEFT JOIN sessions s ON s.project_id = p.project_id
     GROUP BY p.project_id
     ORDER BY p.created_at DESC`,
  );
  return rows.map(r => ({
    projectId: r.project_id,
    name:      r.name,
    createdAt: r.created_at,
    lineCount: r.line_count,
  }));
}

export async function renameProject(projectId: string, name: string): Promise<void> {
  const d = await getDb();
  await d.runAsync('UPDATE projects SET name = ? WHERE project_id = ?', [name.trim(), projectId]);
}

export async function deleteProject(projectId: string): Promise<void> {
  const d = await getDb();
  const sessions = await d.getAllAsync<{ session_id: string }>(
    'SELECT session_id FROM sessions WHERE project_id = ?', [projectId],
  );
  for (const s of sessions) {
    const frames = await d.getAllAsync<{ bin_path: string | null }>(
      'SELECT bin_path FROM frames WHERE session_id = ?', [s.session_id],
    );
    for (const f of frames) {
      if (f.bin_path) { try { await FileSystem.deleteAsync(f.bin_path, { idempotent: true }); } catch {} }
    }
    await d.runAsync('DELETE FROM frames WHERE session_id = ?', [s.session_id]);
  }
  await d.runAsync('DELETE FROM sessions WHERE project_id = ?', [projectId]);
  await d.runAsync('DELETE FROM projects WHERE project_id = ?', [projectId]);
}

// ── Session CRUD ────────────────────────────────────────────────────────────

export async function ensureSession(sessionId: string, projectId?: string | null): Promise<void> {
  const d = await getDb();
  await d.runAsync(
    'INSERT OR IGNORE INTO sessions (session_id, created_at, frame_count, project_id) VALUES (?,?,0,?)',
    [sessionId, Date.now(), projectId ?? null],
  );
  if (projectId) {
    await d.runAsync(
      'UPDATE sessions SET project_id = ? WHERE session_id = ? AND project_id IS NULL',
      [projectId, sessionId],
    );
  }
}

export async function listSessions(): Promise<SessionInfo[]> {
  const d = await getDb();
  const rows = await d.getAllAsync<{
    session_id: string; created_at: number; frame_count: number; project_id: string | null;
  }>(
    'SELECT session_id, created_at, frame_count, project_id FROM sessions ORDER BY created_at DESC',
  );
  return rows.map(r => ({
    sessionId:  r.session_id,
    createdAt:  r.created_at,
    frameCount: r.frame_count,
    projectId:  r.project_id,
  }));
}

export async function listSessionsByProject(projectId: string): Promise<SessionInfo[]> {
  const d = await getDb();
  const rows = await d.getAllAsync<{
    session_id: string; created_at: number; frame_count: number;
  }>(
    'SELECT session_id, created_at, frame_count FROM sessions WHERE project_id = ? ORDER BY created_at DESC',
    [projectId],
  );
  return rows.map(r => ({
    sessionId:  r.session_id,
    createdAt:  r.created_at,
    frameCount: r.frame_count,
    projectId,
  }));
}

export async function assignSessionToProject(sessionId: string, projectId: string | null): Promise<void> {
  const d = await getDb();
  await d.runAsync('UPDATE sessions SET project_id = ? WHERE session_id = ?', [projectId, sessionId]);
}

export async function deleteSession(sessionId: string): Promise<void> {
  const d = await getDb();
  try {
    const files = await FileSystem.readDirectoryAsync(DATA_DIR);
    await Promise.all(
      files
        .filter(f => f.startsWith(sessionId))
        .map(f => FileSystem.deleteAsync(DATA_DIR + f, { idempotent: true })),
    );
  } catch { /* directory may not exist */ }
  await d.runAsync('DELETE FROM frames  WHERE session_id = ?', [sessionId]);
  await d.runAsync('DELETE FROM sessions WHERE session_id = ?', [sessionId]);
}

// ── Frame persistence ────────────────────────────────────────────────────────

export async function persistFrame(frame: MeasurementFrame, sessionId: string): Promise<void> {
  const d = await getDb();
  const m = frame.meta;

  let binPath: string | null = null;
  try {
    const fname = `${sessionId}_f${String(frame.frameId).padStart(6, '0')}.bin`;
    binPath = DATA_DIR + fname;
    await BinLoader.saveBinFile(binPath, frame);
  } catch { binPath = null; }

  await d.runAsync(
    `INSERT OR IGNORE INTO frames (
      frame_id, session_id, ts, utc, lon, lat, alt,
      ch_num, acc_num, gain, gps_st, sd_st, amp_ratio,
      current, temp, batt, roll, pitch, yaw, src_mode, bin_path
    ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
    [
      frame.frameId, sessionId, frame.timestamp,
      m.utc, m.longitude, m.latitude, m.altitude,
      m.channelNum, frame.accNum, frame.gain,
      m.gpsStatus, m.sdStatus, m.ampRatio,
      m.current, m.temperature, m.batteryVolt,
      m.roll, m.pitch, m.yaw, m.sourceMode, binPath,
    ],
  );

  await d.runAsync(
    'UPDATE sessions SET frame_count = frame_count + 1 WHERE session_id = ?',
    [sessionId],
  );
}

export async function loadSessionFrames(sessionId: string): Promise<PersistedFrame[]> {
  const d = await getDb();
  const rows = await d.getAllAsync<any>(
    'SELECT * FROM frames WHERE session_id = ? ORDER BY frame_id DESC',
    [sessionId],
  );
  return rows.map(r => ({
    frameId:   r.frame_id,
    sessionId: r.session_id,
    timestamp: r.ts,
    accNum:    r.acc_num,
    gain:      r.gain,
    binPath:   r.bin_path,
    meta: {
      devId: 0, utc: r.utc,
      longitude: r.lon, latitude: r.lat, altitude: r.alt, height: 0,
      sdStatus: r.sd_st, gpsStatus: r.gps_st, ampRatio: r.amp_ratio,
      roll: r.roll, pitch: r.pitch, yaw: r.yaw,
      channelNum: r.ch_num, current: r.current,
      temperature: r.temp, batteryVolt: r.batt, sourceMode: r.src_mode,
    } as MeasurementMeta,
  }));
}

export async function deleteFrame(frameId: number, sessionId: string): Promise<void> {
  const d = await getDb();
  const row = await d.getFirstAsync<{ bin_path: string | null }>(
    'SELECT bin_path FROM frames WHERE frame_id = ? AND session_id = ?',
    [frameId, sessionId],
  );
  if (row?.bin_path) {
    try { await FileSystem.deleteAsync(row.bin_path, { idempotent: true }); } catch { /* ignore */ }
  }
  await d.runAsync('DELETE FROM frames WHERE frame_id = ? AND session_id = ?', [frameId, sessionId]);
  await d.runAsync('UPDATE sessions SET frame_count = MAX(0, frame_count - 1) WHERE session_id = ?', [sessionId]);
}

export async function getMaxFrameId(sessionId: string): Promise<number> {
  const d = await getDb();
  const row = await d.getFirstAsync<{ max_id: number | null }>(
    'SELECT MAX(frame_id) AS max_id FROM frames WHERE session_id = ?',
    [sessionId],
  );
  return row?.max_id ?? -1;
}

// ── CSV export ──────────────────────────────────────────────────────────────

export async function exportSessionMetaCsv(sessionId: string): Promise<string> {
  const frames = await loadSessionFrames(sessionId);
  if (frames.length === 0) throw new Error('会话无数据');

  const { formatUtc } = await import('../utils/format');

  const maxCh = frames.reduce((m, f) => Math.max(m, f.meta.channelNum), 0);
  const chHeaders = Array.from({ length: maxCh }, (_, i) => `CH${i + 1}_peak_uV`).join(',');
  const header = `FrameID,UTC,Longitude,Latitude,Altitude,GPS,SD,AmpRatio,Current,Temp,Battery,Roll,Pitch,Yaw,${chHeaders}`;

  const rows: string[] = [];
  for (const f of frames) {
    const m = f.meta;
    let peaks: string[] = Array(maxCh).fill('');
    if (f.binPath) {
      const bin = await BinLoader.loadBinFile(f.binPath);
      if (bin) {
        peaks = Array.from({ length: maxCh }, (_, i) => {
          const ch = bin.adcUV[i];
          if (!ch) return '';
          const pk = ch.reduce((mx, v) => Math.max(mx, Math.abs(v)), 0);
          return pk.toFixed(4);
        });
      }
    }
    rows.push([
      f.frameId, formatUtc(m.utc),
      m.longitude.toFixed(7), m.latitude.toFixed(7), m.altitude.toFixed(2),
      m.gpsStatus, m.sdStatus, m.ampRatio, m.current, m.temperature, m.batteryVolt,
      m.roll.toFixed(2), m.pitch.toFixed(2), m.yaw.toFixed(2),
      ...peaks,
    ].join(','));
  }

  const csv = [header, ...rows].join('\n');
  await ensureDataDir();
  const path = DATA_DIR + `TEM_${sessionId}.csv`;
  await FileSystem.writeAsStringAsync(path, csv, { encoding: FileSystem.EncodingType.UTF8 });
  return path;
}

// ── .tem export ─────────────────────────────────────────────────────────────

export async function exportProject(
  projectId:    string,
  deviceConfig: SetupConfig,
  gateConfig:   GateConfig,
  onProgress?:  (current: number, total: number) => void,
): Promise<string> {
  const d = await getDb();

  const proj = await d.getFirstAsync<{ name: string; created_at: number }>(
    'SELECT name, created_at FROM projects WHERE project_id = ?',
    [projectId],
  );
  if (!proj) throw new Error('工程不存在');

  const sessions = await listSessionsByProject(projectId);

  // Collect all frames + bin bytes
  const bins: Record<string, Uint8Array> = {};
  const sessionManifests: TemBundle.SessionManifest[] = [];
  let totalFrames = sessions.reduce((s, x) => s + x.frameCount, 0);
  let done = 0;

  for (const session of sessions) {
    const frames = await loadSessionFrames(session.sessionId);
    for (const f of frames) {
      if (f.binPath) {
        const bytes = await BinLoader.readFileBytes(f.binPath);
        if (bytes) {
          const relPath = TemBundle.bundleBinPath(session.sessionId, f.frameId);
          bins[relPath] = bytes;
        }
      }
      onProgress?.(++done, totalFrames);
    }
    sessionManifests.push({
      id:            session.sessionId,
      created_at:    session.createdAt,
      frame_count:   session.frameCount,
      device_config: deviceConfig,
    });
  }

  const manifest: TemBundle.ProjectManifest = {
    schema_version: '1.0',
    exported_at:    new Date().toISOString(),
    project:        { id: projectId, name: proj.name, created_at: proj.created_at },
    device_config:  deviceConfig,
    gate_config:    gateConfig,
    sessions:       sessionManifests,
  };

  const bundleBytes = await TemBundle.packBundle(manifest, bins);

  await ensureDataDir();
  const safeName = sanitizeName(proj.name);
  const ts = new Date().toISOString().slice(0, 10).replace(/-/g, '');
  const outPath = DATA_DIR + `TEM_${safeName}_${ts}.tem`;
  await BinLoader.writeFileBytes(outPath, bundleBytes);
  return outPath;
}

// ── .tem import ─────────────────────────────────────────────────────────────

export interface ImportResult {
  projectId: string;
  projectName: string;
  sessionCount: number;
  frameCount: number;
}

export async function importProject(
  bundleUri:   string,
  onProgress?: (current: number, total: number) => void,
): Promise<ImportResult> {
  await ensureDataDir();
  const d = await getDb();

  // Copy to temp path (handles content:// URIs on Android)
  const tmpPath = DATA_DIR + `tmp_import_${Date.now()}.tem`;
  try {
    await FileSystem.copyAsync({ from: bundleUri, to: tmpPath });
  } catch {
    // URI may already be a file:// path — try reading directly
  }

  let bundleBytes: Uint8Array | null = null;
  try {
    bundleBytes = await BinLoader.readFileBytes(tmpPath);
  } catch { /* fall through */ }

  if (!bundleBytes) {
    bundleBytes = await BinLoader.readFileBytes(bundleUri);
  }
  if (!bundleBytes) throw new Error('无法读取 .tem 文件');

  // Clean up temp file (don't await — non-critical)
  FileSystem.deleteAsync(tmpPath, { idempotent: true }).catch(() => {});

  const { manifest, bins } = await TemBundle.parseBundle(bundleBytes);

  // Allocate new project ID to avoid collision
  const newProjectId = generateProjectId();
  await d.runAsync(
    'INSERT INTO projects (project_id, name, created_at) VALUES (?,?,?)',
    [newProjectId, manifest.project.name, manifest.project.created_at],
  );

  const totalFrames = Object.keys(bins).length;
  let done = 0;
  let importedFrames = 0;

  for (const session of manifest.sessions) {
    // Use original session ID if not already in DB; otherwise suffix with timestamp
    const existing = await d.getFirstAsync<{ session_id: string }>(
      'SELECT session_id FROM sessions WHERE session_id = ?',
      [session.id],
    );
    const newSessionId = existing ? `${session.id}_${Date.now()}` : session.id;

    await d.runAsync(
      'INSERT INTO sessions (session_id, created_at, frame_count, project_id) VALUES (?,?,0,?)',
      [newSessionId, session.created_at, newProjectId],
    );

    let sessionFrameCount = 0;
    for (const [relPath, binBytes] of Object.entries(bins)) {
      const parsed = TemBundle.parseBinPath(relPath);
      if (!parsed || parsed.sessionId !== session.id) continue;

      const { frameId } = parsed;
      const meta = BinLoader.decodeBin(binBytes);
      if (!meta) { onProgress?.(++done, totalFrames); continue; }

      // Write bin file to DATA_DIR
      const fname = `${newSessionId}_f${String(frameId).padStart(6, '0')}.bin`;
      const binPath = DATA_DIR + fname;
      await BinLoader.writeFileBytes(binPath, binBytes);

      // Insert frame record
      await d.runAsync(
        `INSERT OR IGNORE INTO frames (
          frame_id, session_id, ts, utc, lon, lat, alt,
          ch_num, acc_num, gain, gps_st, sd_st, amp_ratio,
          current, temp, batt, roll, pitch, yaw, src_mode, bin_path
        ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
        [
          frameId, newSessionId, meta.timestamp,
          meta.utc, meta.longitude, meta.latitude, meta.altitude,
          meta.channelNum, meta.accNum, AMP_GAIN[meta.ampRatio] ?? 1,
          meta.gpsStatus, meta.sdStatus, meta.ampRatio,
          meta.current, meta.temperature, meta.batteryVolt,
          meta.roll, meta.pitch, meta.yaw, meta.srcMode, binPath,
        ],
      );

      sessionFrameCount++;
      importedFrames++;
      onProgress?.(++done, totalFrames);
    }

    await d.runAsync('UPDATE sessions SET frame_count = ? WHERE session_id = ?', [sessionFrameCount, newSessionId]);
  }

  return {
    projectId:    newProjectId,
    projectName:  manifest.project.name,
    sessionCount: manifest.sessions.length,
    frameCount:   importedFrames,
  };
}
