"""Task 05 - Convert the CSV corpus to SQLite.

Builds ./out/sqlite/tribo.db: variant B key schema (composite natural PKs
on bulk tables, WITHOUT ROWID, enforced foreign keys - see
docs/research/Tribology_FK_Architecture.html), Q1-Q7 indexes created after
the load, track_summary precomputed. Every consumed CSV is verified
against MANIFEST.csv. Appends the db size to storage_sizes.csv and writes
./out/.done/05.ok. Spec: docs/specs/05_convert_sqlite.md.
"""

from __future__ import annotations

import logging
import sqlite3
import sys
from pathlib import Path

from common.corpus import CorpusReader, ValidationError
from common.pipeline import (
	check_dependencies,
	load_lab_config,
	parse_task_args,
	process_metrics,
	remove_stale_marker,
	write_marker,
)
from common.relational import COLUMNS, TABLES, expected_counts, stream_rows
from common.storage_sizes import update_storage_sizes

logger = logging.getLogger(__name__)

TASK_ID = "05"
DEPENDS_ON = ["01", "03"]

BATCH_ROWS = 50000  # rows per executemany transaction, spec 05
LOAD_PRAGMAS = [
	"PRAGMA journal_mode=OFF",
	"PRAGMA synchronous=OFF",
	"PRAGMA cache_size=-262144",
	"PRAGMA foreign_keys=ON",
]

DDL = [
	"""CREATE TABLE instruments (
		instrument_id INTEGER PRIMARY KEY,
		instrument_code TEXT NOT NULL UNIQUE,
		name TEXT NOT NULL,
		role TEXT NOT NULL
	)""",
	"""CREATE TABLE batches (
		batch_id INTEGER PRIMARY KEY,
		batch_code TEXT NOT NULL UNIQUE,
		pt_gun_tilt_deg INTEGER NOT NULL,
		au_gun_tilt_deg INTEGER NOT NULL,
		pt_power_w INTEGER NOT NULL,
		au_power_w INTEGER NOT NULL,
		pt_discharge_v INTEGER NOT NULL,
		au_discharge_v INTEGER NOT NULL,
		deposition_date TEXT NOT NULL
	)""",
	"""CREATE TABLE runs (
		run_id INTEGER PRIMARY KEY,
		run_code TEXT NOT NULL UNIQUE,
		environment TEXT NOT NULL,
		date TEXT NOT NULL,
		plates INTEGER NOT NULL,
		operator TEXT NOT NULL
	)""",
	"""CREATE TABLE wafers (
		wafer_id INTEGER PRIMARY KEY,
		wafer_code TEXT NOT NULL UNIQUE,
		batch_id INTEGER NOT NULL REFERENCES batches(batch_id),
		wafer_index INTEGER NOT NULL,
		deposition_date TEXT NOT NULL,
		coupons INTEGER NOT NULL,
		friction_coupons INTEGER NOT NULL,
		reserve_coupons INTEGER NOT NULL
	)""",
	"""CREATE TABLE coupons (
		coupon_id INTEGER PRIMARY KEY,
		coupon_code TEXT NOT NULL UNIQUE,
		wafer_id INTEGER NOT NULL REFERENCES wafers(wafer_id),
		batch_id INTEGER NOT NULL REFERENCES batches(batch_id),
		grid_row INTEGER NOT NULL,
		grid_col INTEGER NOT NULL,
		thickness_um REAL NOT NULL,
		ra_nm REAL NOT NULL,
		au_wtpct_mean REAL NOT NULL,
		run_id INTEGER REFERENCES runs(run_id),
		plate INTEGER,
		probe INTEGER,
		square INTEGER
	)""",
	"""CREATE TABLE tracks (
		track_id INTEGER PRIMARY KEY,
		track_code TEXT NOT NULL UNIQUE,
		coupon_id INTEGER NOT NULL REFERENCES coupons(coupon_id),
		run_id INTEGER NOT NULL REFERENCES runs(run_id),
		environment TEXT NOT NULL,
		load_mn REAL NOT NULL,
		stroke_mm REAL NOT NULL,
		speed_mm_s REAL NOT NULL,
		counterface_id TEXT NOT NULL,
		started_at TEXT NOT NULL
	)""",
	"""CREATE TABLE simtra_profiles (
		batch_id INTEGER NOT NULL REFERENCES batches(batch_id),
		row_no INTEGER NOT NULL,
		angle_deg REAL NOT NULL,
		energy_ev REAL NOT NULL,
		pt_flux REAL NOT NULL,
		au_flux REAL NOT NULL,
		PRIMARY KEY (batch_id, row_no)
	) WITHOUT ROWID""",
	"""CREATE TABLE xrf_points (
		coupon_id INTEGER NOT NULL REFERENCES coupons(coupon_id),
		grid_x INTEGER NOT NULL,
		grid_y INTEGER NOT NULL,
		pt_wtpct REAL NOT NULL,
		au_wtpct REAL NOT NULL,
		PRIMARY KEY (coupon_id, grid_x, grid_y)
	) WITHOUT ROWID""",
	"""CREATE TABLE profilometry_points (
		coupon_id INTEGER NOT NULL REFERENCES coupons(coupon_id),
		grid_x INTEGER NOT NULL,
		grid_y INTEGER NOT NULL,
		thickness_um REAL NOT NULL,
		PRIMARY KEY (coupon_id, grid_x, grid_y)
	) WITHOUT ROWID""",
	"""CREATE TABLE nanoindentation (
		coupon_id INTEGER NOT NULL REFERENCES coupons(coupon_id),
		indent_id INTEGER NOT NULL,
		x_um REAL NOT NULL,
		y_um REAL NOT NULL,
		hardness_gpa REAL NOT NULL,
		reduced_modulus_gpa REAL NOT NULL,
		max_load_mn REAL NOT NULL,
		PRIMARY KEY (coupon_id, indent_id)
	) WITHOUT ROWID""",
	"""CREATE TABLE afm (
		coupon_id INTEGER PRIMARY KEY REFERENCES coupons(coupon_id),
		ra_nm REAL NOT NULL,
		rq_nm REAL NOT NULL,
		image_file TEXT NOT NULL
	)""",
	"""CREATE TABLE friction_cycles (
		track_id INTEGER NOT NULL REFERENCES tracks(track_id),
		cycle INTEGER NOT NULL,
		cof REAL NOT NULL,
		PRIMARY KEY (track_id, cycle)
	) WITHOUT ROWID""",
	"""CREATE TABLE friction_loop_points (
		track_id INTEGER NOT NULL REFERENCES tracks(track_id),
		cycle INTEGER NOT NULL,
		pt INTEGER NOT NULL,
		position_um REAL NOT NULL,
		friction_force_mn REAL NOT NULL,
		PRIMARY KEY (track_id, cycle, pt)
	) WITHOUT ROWID""",
	"""CREATE TABLE wear (
		track_id INTEGER PRIMARY KEY REFERENCES tracks(track_id),
		wear_volume_um3 REAL NOT NULL,
		k_archard REAL NOT NULL,
		sliding_distance_m REAL NOT NULL
	)""",
	"""CREATE TABLE track_summary (
		track_id INTEGER PRIMARY KEY REFERENCES tracks(track_id),
		cof_ss_mean REAL NOT NULL,
		cof_ss_std REAL NOT NULL,
		run_in_cycles INTEGER NOT NULL
	)""",
]

INDEX_DDL = [
	"CREATE INDEX ix_coupons_au_wtpct_mean ON coupons(au_wtpct_mean)",  # Q2 selective filter
	"CREATE INDEX ix_tracks_run_id ON tracks(run_id)",  # Q5/Q7 joins to runs
	"CREATE INDEX ix_tracks_coupon_id ON tracks(coupon_id)",  # Q3/Q6 joins to coupons
	"CREATE INDEX ix_runs_environment ON runs(environment)",  # Q7 grouping
	"CREATE INDEX ix_tracks_environment_load ON tracks(environment, load_mn)",  # Q4 filter
]


class SqliteLoader:
	def __init__(self, db_path: Path) -> None:
		db_path.parent.mkdir(parents=True, exist_ok=True)
		self.conn = sqlite3.connect(db_path)
		for pragma in LOAD_PRAGMAS:
			self.conn.execute(pragma)
		for ddl in DDL:
			self.conn.execute(ddl)
		self.inserts = {
			t: f"INSERT INTO {t} VALUES ({','.join('?' * len(COLUMNS[t]))})" for t in TABLES
		}
		self.buffers: dict[str, list[tuple]] = {t: [] for t in TABLES}

	def load(self, cfg: dict, reader: CorpusReader) -> None:
		pending = 0
		for table, row in stream_rows(cfg, reader):
			self.buffers[table].append(row)
			pending += 1
			if pending >= BATCH_ROWS:
				self.flush_all()
				pending = 0
		self.flush_all()

	def flush_all(self) -> None:
		# TABLES is FK-dependency ordered: parents flush before children.
		for table in TABLES:
			buf = self.buffers[table]
			if buf:
				self.conn.executemany(self.inserts[table], buf)
				buf.clear()
		self.conn.commit()

	def finalize(self) -> None:
		for ddl in INDEX_DDL:
			self.conn.execute(ddl)
		self.conn.commit()
		self.conn.execute("PRAGMA journal_mode=WAL")
		self.conn.execute("ANALYZE")
		self.conn.execute("VACUUM")
		logger.info("indexes created, WAL enabled, ANALYZE + VACUUM done")

	def validate(self, cfg: dict) -> dict[str, int]:
		counts: dict[str, int] = {}
		for table, exp in expected_counts(cfg).items():
			got = self.conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
			counts[table] = got
			if got != exp:
				raise ValidationError(f"row count mismatch: {table}: loaded {got} != expected {exp}")
		reserve = self.conn.execute("SELECT COUNT(*) FROM coupons WHERE run_id IS NULL").fetchone()[0]
		if reserve != cfg["friction_assignment"]["reserve_coupons_total"]:
			raise ValidationError(f"reserve coupons: {reserve} != expected")
		fk_violations = self.conn.execute("PRAGMA foreign_key_check").fetchall()
		if fk_violations:
			raise ValidationError(f"foreign_key_check reported {len(fk_violations)} violations")
		logger.info("all %d table counts match, %d reserve coupons, foreign_key_check clean", len(counts), reserve)
		return counts


def main() -> int:
	args = parse_task_args("Task 05: convert the CSV corpus to SQLite")
	out_root: Path = args.out_root
	db_path = out_root / "sqlite" / "tribo.db"
	try:
		check_dependencies(out_root, DEPENDS_ON)
		remove_stale_marker(out_root, TASK_ID)
		for stale in (db_path, db_path.with_suffix(".db-wal"), db_path.with_suffix(".db-shm")):
			if stale.exists():
				logger.info("re-run: removing %s", stale)
				stale.unlink()
		cfg = load_lab_config(out_root)

		loader = SqliteLoader(db_path)
		loader.load(cfg, CorpusReader(out_root / "csv"))
		loader.finalize()
		counts = loader.validate(cfg)
		loader.conn.close()

		db_bytes = db_path.stat().st_size
		update_storage_sizes(out_root / "bench", "sqlite", [("db", db_bytes)])
		entries = {
			"db_file": db_path.as_posix(),
			"db_bytes": db_bytes,
			"tables": len(counts),
			"total_rows": sum(counts.values()),
			"friction_cycles": counts["friction_cycles"],
			"friction_loop_points": counts["friction_loop_points"],
			"tracks": counts["tracks"],
			"track_summary": counts["track_summary"],
		}
		entries.update(process_metrics())
		marker_path = write_marker(out_root, TASK_ID, entries)
	except Exception:
		logger.critical("task %s failed", TASK_ID, exc_info=True)
		return 1

	print(f"task 05 ok: {db_path} ({db_bytes / (1024 * 1024):.1f} MiB, {len(counts)} tables, {sum(counts.values())} rows)")
	print(f"bulk rows: cycles={counts['friction_cycles']} loop_points={counts['friction_loop_points']} "
		f"xrf={counts['xrf_points']} track_summary={counts['track_summary']}")
	print(f"marker: {marker_path}")
	return 0


if __name__ == "__main__":
	sys.exit(main())
