"""Export the local DMS demo database rows and referenced storage files.

The connection URL is read from an existing dotenv file and is never written
to the export. The resulting SQL is data-only and targets an already migrated
empty DMS database.
"""

from __future__ import annotations

import argparse
import hashlib
import json
import shutil
from datetime import date, datetime, time
from decimal import Decimal
from pathlib import Path
from typing import Any

from dotenv import dotenv_values
from sqlalchemy import MetaData, Table, create_engine, select


TABLES = (
    "sys_organization",
    "sys_user",
    "doc_category",
    "doc_document",
    "doc_attachment_binding",
    "doc_permission",
    "sys_audit_log",
)


def sql_literal(value: Any) -> str:
    if value is None:
        return "NULL"
    if isinstance(value, bool):
        return "1" if value else "0"
    if isinstance(value, (int, Decimal)):
        return str(value)
    if isinstance(value, float):
        return repr(value)
    if isinstance(value, (datetime, date, time)):
        value = value.isoformat(sep=" ") if isinstance(value, datetime) else value.isoformat()
    if isinstance(value, (dict, list)):
        value = json.dumps(value, ensure_ascii=False, separators=(",", ":"))
    if isinstance(value, bytes):
        return "X'" + value.hex() + "'"
    escaped = str(value).replace("\\", "\\\\").replace("'", "''")
    return "'" + escaped + "'"


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as stream:
        for chunk in iter(lambda: stream.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def export(env_file: Path, source_storage: Path, output: Path) -> None:
    config = dotenv_values(env_file)
    database_url = (config.get("DMS_DATABASE_URL") or "").strip()
    if not database_url:
        raise RuntimeError("DMS_DATABASE_URL is missing from the source dotenv file")

    output.mkdir(parents=True, exist_ok=True)
    storage_output = output / "storage"
    storage_output.mkdir(exist_ok=True)

    engine = create_engine(database_url, pool_pre_ping=True)
    metadata = MetaData()
    reflected = {
        name: Table(name, metadata, autoload_with=engine)
        for name in TABLES
    }

    rows_by_table: dict[str, list[dict[str, Any]]] = {}
    with engine.connect() as connection:
        for name in TABLES:
            rows_by_table[name] = [dict(row) for row in connection.execute(select(reflected[name])).mappings()]
    engine.dispose()

    document_paths: dict[str, dict[str, Any]] = {}
    for row in rows_by_table["doc_document"]:
        relative = str(row.get("file_relative_path") or "").strip().replace("\\", "/")
        if not relative:
            continue
        if relative.startswith("/") or ".." in Path(relative).parts:
            raise RuntimeError(f"Unsafe storage path in database: {relative}")
        source = source_storage / Path(relative)
        if not source.is_file():
            raise RuntimeError(f"Referenced storage file is missing: {relative}")
        expected_size = row.get("file_size")
        if expected_size is not None and source.stat().st_size != int(expected_size):
            raise RuntimeError(f"Referenced storage file has unexpected size: {relative}")
        actual_hash = sha256(source)
        expected_hash = str(row.get("file_hash") or "").lower()
        if expected_hash and actual_hash != expected_hash:
            raise RuntimeError(f"Referenced storage file has unexpected SHA-256: {relative}")
        destination = storage_output / Path(relative)
        destination.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(source, destination)
        document_paths[relative] = {
            "size": source.stat().st_size,
            "sha256": actual_hash,
        }

    sql_path = output / "full_demo_data.sql"
    with sql_path.open("w", encoding="utf-8", newline="\n") as sql:
        sql.write("-- DMS full demo data export (data only; no schema or secrets)\n")
        sql.write("SET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS=0;\nSTART TRANSACTION;\n")
        for name in reversed(TABLES):
            sql.write(f"DELETE FROM `{name}`;\n")
        for name in TABLES:
            table = reflected[name]
            # MySQL generated columns (for example active_marker) must not be
            # supplied explicitly in INSERT statements.
            columns = [
                column.name
                for column in table.columns
                if column.computed is None
            ]
            quoted_columns = ", ".join(f"`{column}`" for column in columns)
            for row in rows_by_table[name]:
                values = ", ".join(sql_literal(row.get(column)) for column in columns)
                sql.write(f"INSERT INTO `{name}` ({quoted_columns}) VALUES ({values});\n")
        sql.write("COMMIT;\nSET FOREIGN_KEY_CHECKS=1;\n")

    manifest = {
        "format": "DMS full demo data v1",
        "database_tables": {name: len(rows_by_table[name]) for name in TABLES},
        "storage_file_count": len(document_paths),
        "storage_total_bytes": sum(item["size"] for item in document_paths.values()),
        "storage_files": document_paths,
        "sql_sha256": sha256(sql_path),
    }
    (output / "manifest.json").write_text(
        json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    print(json.dumps(manifest, ensure_ascii=False, indent=2))


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--env-file", type=Path, required=True)
    parser.add_argument("--storage", type=Path, required=True)
    parser.add_argument("--output", type=Path, required=True)
    arguments = parser.parse_args()
    export(arguments.env_file.resolve(), arguments.storage.resolve(), arguments.output.resolve())


if __name__ == "__main__":
    main()
