"""
Doit Agent - Persistence Layer (SQLite)
Handles all database operations with async support.
"""
from __future__ import annotations

import json
import logging
from datetime import datetime
from pathlib import Path
from typing import Any, Optional

import aiosqlite

from core.config import DB_PATH, DB_SCHEMA_VERSION

logger = logging.getLogger("doit.db")

SCHEMA_SQL = """
PRAGMA journal_mode=WAL;
PRAGMA foreign_keys=ON;

CREATE TABLE IF NOT EXISTS migrations (
    version     INTEGER PRIMARY KEY,
    applied_at  TEXT NOT NULL,
    description TEXT
);

CREATE TABLE IF NOT EXISTS config (
    key         TEXT PRIMARY KEY,
    value       TEXT NOT NULL,
    updated_at  TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS tasks (
    id          TEXT PRIMARY KEY,
    type        TEXT NOT NULL,
    status      TEXT NOT NULL DEFAULT 'pending',
    payload     TEXT NOT NULL DEFAULT '{}',
    result      TEXT,
    error       TEXT,
    retry_count INTEGER NOT NULL DEFAULT 0,
    max_retries INTEGER NOT NULL DEFAULT 3,
    timeout     INTEGER NOT NULL DEFAULT 300,
    created_at  TEXT NOT NULL,
    updated_at  TEXT NOT NULL,
    started_at  TEXT,
    completed_at TEXT
);

CREATE TABLE IF NOT EXISTS schedules (
    id          TEXT PRIMARY KEY,
    name        TEXT NOT NULL,
    description TEXT,
    cron_expr   TEXT,
    interval_s  INTEGER,
    next_run    TEXT,
    last_run    TEXT,
    enabled     INTEGER NOT NULL DEFAULT 1,
    task_type   TEXT NOT NULL,
    task_payload TEXT NOT NULL DEFAULT '{}',
    run_count   INTEGER NOT NULL DEFAULT 0,
    created_at  TEXT NOT NULL,
    updated_at  TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS logs (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    level       TEXT NOT NULL,
    module      TEXT NOT NULL,
    message     TEXT NOT NULL,
    extra       TEXT,
    created_at  TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS audit_log (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    action      TEXT NOT NULL,
    actor       TEXT NOT NULL DEFAULT 'system',
    target      TEXT,
    details     TEXT,
    status      TEXT NOT NULL DEFAULT 'ok',
    created_at  TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS plugin_registry (
    name        TEXT PRIMARY KEY,
    version     TEXT NOT NULL,
    path        TEXT NOT NULL,
    enabled     INTEGER NOT NULL DEFAULT 1,
    tools       TEXT NOT NULL DEFAULT '[]',
    installed_at TEXT NOT NULL,
    updated_at  TEXT NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
CREATE INDEX IF NOT EXISTS idx_tasks_created ON tasks(created_at);
CREATE INDEX IF NOT EXISTS idx_logs_created ON logs(created_at);
CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_log(created_at);
"""


class Database:
    """Async SQLite database manager."""

    def __init__(self, path: Path = DB_PATH):
        self.path = path
        self._db: Optional[aiosqlite.Connection] = None

    async def connect(self) -> None:
        """Open connection and initialize schema."""
        self.path.parent.mkdir(parents=True, exist_ok=True)
        self._db = await aiosqlite.connect(str(self.path))
        self._db.row_factory = aiosqlite.Row
        await self._db.executescript(SCHEMA_SQL)
        await self._db.commit()
        await self._run_migrations()
        logger.info("Database connected: %s", self.path)

    async def close(self) -> None:
        if self._db:
            await self._db.close()
            self._db = None

    async def _run_migrations(self) -> None:
        """Apply any pending schema migrations."""
        async with self._db.execute("SELECT MAX(version) as v FROM migrations") as cur:
            row = await cur.fetchone()
            current = row["v"] if row and row["v"] else 0

        migrations = _get_migrations()
        for version, description, sql in migrations:
            if version > current:
                logger.info("Applying migration v%d: %s", version, description)
                await self._db.executescript(sql)
                await self._db.execute(
                    "INSERT INTO migrations (version, applied_at, description) VALUES (?,?,?)",
                    (version, _now(), description),
                )
                await self._db.commit()

    # ── Config ────────────────────────────────────────────────────────────
    async def config_set(self, key: str, value: Any) -> None:
        v = json.dumps(value) if not isinstance(value, str) else value
        await self._db.execute(
            "INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?,?,?)",
            (key, v, _now()),
        )
        await self._db.commit()

    async def config_get(self, key: str, default: Any = None) -> Any:
        async with self._db.execute("SELECT value FROM config WHERE key=?", (key,)) as cur:
            row = await cur.fetchone()
            if not row:
                return default
            try:
                return json.loads(row["value"])
            except (json.JSONDecodeError, TypeError):
                return row["value"]

    async def config_all(self) -> dict:
        async with self._db.execute("SELECT key, value FROM config") as cur:
            rows = await cur.fetchall()
        result = {}
        for row in rows:
            try:
                result[row["key"]] = json.loads(row["value"])
            except Exception:
                result[row["key"]] = row["value"]
        return result

    # ── Tasks ─────────────────────────────────────────────────────────────
    async def task_insert(self, task: dict) -> None:
        await self._db.execute(
            """INSERT INTO tasks
               (id, type, status, payload, retry_count, max_retries, timeout, created_at, updated_at)
               VALUES (:id,:type,:status,:payload,:retry_count,:max_retries,:timeout,:created_at,:updated_at)""",
            {
                "id": task["id"],
                "type": task["type"],
                "status": task.get("status", "pending"),
                "payload": json.dumps(task.get("payload", {})),
                "retry_count": task.get("retry_count", 0),
                "max_retries": task.get("max_retries", 3),
                "timeout": task.get("timeout", 300),
                "created_at": _now(),
                "updated_at": _now(),
            },
        )
        await self._db.commit()

    async def task_update(self, task_id: str, **fields) -> None:
        fields["updated_at"] = _now()
        if "result" in fields and not isinstance(fields["result"], str):
            fields["result"] = json.dumps(fields["result"])
        set_clause = ", ".join(f"{k}=:{k}" for k in fields)
        fields["task_id"] = task_id
        await self._db.execute(f"UPDATE tasks SET {set_clause} WHERE id=:task_id", fields)
        await self._db.commit()

    async def task_get(self, task_id: str) -> Optional[dict]:
        async with self._db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)) as cur:
            row = await cur.fetchone()
            return dict(row) if row else None

    async def tasks_list(self, status: Optional[str] = None, limit: int = 50) -> list[dict]:
        if status:
            sql = "SELECT * FROM tasks WHERE status=? ORDER BY created_at DESC LIMIT ?"
            args = (status, limit)
        else:
            sql = "SELECT * FROM tasks ORDER BY created_at DESC LIMIT ?"
            args = (limit,)
        async with self._db.execute(sql, args) as cur:
            rows = await cur.fetchall()
        return [dict(r) for r in rows]

    async def tasks_pending(self) -> list[dict]:
        async with self._db.execute(
            "SELECT * FROM tasks WHERE status='pending' ORDER BY created_at ASC"
        ) as cur:
            rows = await cur.fetchall()
        return [dict(r) for r in rows]

    # ── Schedules ─────────────────────────────────────────────────────────
    async def schedule_upsert(self, schedule: dict) -> None:
        await self._db.execute(
            """INSERT OR REPLACE INTO schedules
               (id,name,description,cron_expr,interval_s,next_run,last_run,enabled,
                task_type,task_payload,run_count,created_at,updated_at)
               VALUES
               (:id,:name,:description,:cron_expr,:interval_s,:next_run,:last_run,:enabled,
                :task_type,:task_payload,:run_count,:created_at,:updated_at)""",
            {
                "id": schedule["id"],
                "name": schedule["name"],
                "description": schedule.get("description", ""),
                "cron_expr": schedule.get("cron_expr"),
                "interval_s": schedule.get("interval_s"),
                "next_run": schedule.get("next_run"),
                "last_run": schedule.get("last_run"),
                "enabled": 1 if schedule.get("enabled", True) else 0,
                "task_type": schedule["task_type"],
                "task_payload": json.dumps(schedule.get("task_payload", {})),
                "run_count": schedule.get("run_count", 0),
                "created_at": schedule.get("created_at", _now()),
                "updated_at": _now(),
            },
        )
        await self._db.commit()

    async def schedules_list(self) -> list[dict]:
        async with self._db.execute("SELECT * FROM schedules ORDER BY name") as cur:
            rows = await cur.fetchall()
        return [dict(r) for r in rows]

    async def schedule_delete(self, schedule_id: str) -> bool:
        cur = await self._db.execute("DELETE FROM schedules WHERE id=?", (schedule_id,))
        await self._db.commit()
        return cur.rowcount > 0

    # ── Logs ──────────────────────────────────────────────────────────────
    async def log_insert(self, level: str, module: str, message: str, extra: Any = None) -> None:
        await self._db.execute(
            "INSERT INTO logs (level, module, message, extra, created_at) VALUES (?,?,?,?,?)",
            (level, module, message, json.dumps(extra) if extra else None, _now()),
        )
        await self._db.commit()

    async def logs_recent(self, limit: int = 50) -> list[dict]:
        async with self._db.execute(
            "SELECT * FROM logs ORDER BY created_at DESC LIMIT ?", (limit,)
        ) as cur:
            rows = await cur.fetchall()
        return [dict(r) for r in rows]

    # ── Audit ─────────────────────────────────────────────────────────────
    async def audit(self, action: str, actor: str = "system", target: str = None,
                    details: Any = None, status: str = "ok") -> None:
        await self._db.execute(
            "INSERT INTO audit_log (action, actor, target, details, status, created_at) VALUES (?,?,?,?,?,?)",
            (action, actor, target, json.dumps(details) if details else None, status, _now()),
        )
        await self._db.commit()

    async def audit_export(self, fmt: str = "json") -> str:
        async with self._db.execute("SELECT * FROM audit_log ORDER BY created_at DESC") as cur:
            rows = await cur.fetchall()
        data = [dict(r) for r in rows]
        if fmt == "json":
            return json.dumps(data, indent=2)
        elif fmt == "csv":
            if not data:
                return ""
            import csv, io
            out = io.StringIO()
            writer = csv.DictWriter(out, fieldnames=data[0].keys())
            writer.writeheader()
            writer.writerows(data)
            return out.getvalue()
        return json.dumps(data)

    # ── Plugin Registry ───────────────────────────────────────────────────
    async def plugin_register(self, plugin: dict) -> None:
        await self._db.execute(
            """INSERT OR REPLACE INTO plugin_registry
               (name,version,path,enabled,tools,installed_at,updated_at)
               VALUES (?,?,?,?,?,?,?)""",
            (
                plugin["name"], plugin["version"], plugin["path"],
                1 if plugin.get("enabled", True) else 0,
                json.dumps(plugin.get("tools", [])),
                plugin.get("installed_at", _now()), _now(),
            ),
        )
        await self._db.commit()

    async def plugins_list(self) -> list[dict]:
        async with self._db.execute("SELECT * FROM plugin_registry") as cur:
            rows = await cur.fetchall()
        return [dict(r) for r in rows]


def _now() -> str:
    return datetime.utcnow().isoformat()


def _get_migrations() -> list[tuple[int, str, str]]:
    """Return list of (version, description, sql) migrations."""
    return [
        (
            1,
            "Initial schema",
            "-- base schema already applied",
        ),
    ]


# Singleton
_db_instance: Optional[Database] = None


async def get_db() -> Database:
    global _db_instance
    if _db_instance is None:
        _db_instance = Database()
        await _db_instance.connect()
    return _db_instance
