# db_repo.py
# Updated DB helper: include duplicate/rejection metadata and pdf/txn md5s in list_files.
# Keeps existing APIs (list_files, get_fields, list_audit, get_s3_key, upsert_file, upsert_output, find_by_md5, list_rejected, list_runs, get_files_by_run)
# but list_files now returns additional columns useful to the API/UI:
#   pdf_md5, txn_md5, rejection_reason, rejected_for, is_duplicate (0/1)
#
# Note: This file assumes MS SQL Server + pyodbc. Environment variables control connection.

import os
import json
import pyodbc
from typing import List, Dict, Optional

# ----------------------------- connection -----------------------------
def _connect():
    # Default to 17 to match your installed driver list
    driver = os.getenv("MSSQL_DRIVER", "ODBC Driver 17 for SQL Server")
    server = os.getenv("MSSQL_SERVER", "localhost")
    db     = os.getenv("MSSQL_DATABASE", "VITU")
    user   = os.getenv("MSSQL_USER")
    pwd    = os.getenv("MSSQL_PASSWORD")
    if not (user and pwd):
        raise RuntimeError("Missing MSSQL_USER or MSSQL_PASSWORD in environment")
    cn = pyodbc.connect(
        f"DRIVER={{{driver}}};SERVER={server};DATABASE={db};UID={user};PWD={pwd};"
        "Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=30;"
    )
    cn.autocommit = True
    return cn

def _rows_to_dicts(cur, cols):
    return [dict(zip(cols, row)) for row in cur.fetchall()]

def _fetch_first_row_from_cursor(cur):
    """
    Defensive helper to fetch the first available row from a cursor even if the driver
    emits non-query resultsets before the SELECT we care about. Loops through resultsets
    using nextset() until a row is available or no more resultsets.
    Returns the row tuple or None.
    """
    try:
        row = cur.fetchone()
        if row:
            return row
    except Exception:
        # some drivers raise when trying to fetch non-query results; continue to nextset
        pass

    # iterate through result sets
    try:
        while cur.nextset():
            try:
                row = cur.fetchone()
                if row:
                    return row
            except Exception:
                continue
    except Exception:
        # nextset may raise if driver doesn't support it or no more sets; fall through
        pass
    return None

# ----------------------------- reads (used by API) -----------------------------
def list_files(from_date: str, to_date: str, state: Optional[str]) -> List[Dict]:
    """
    Rows compatible with your frontend /api/files:
      [ {
          date, state, prefix, filename, pages, started, ended, durationSec, status,
          guid, run_id, pdf_md5, txn_md5, rejection_reason, rejected_for, is_duplicate,
          is_suspect_data, suspect_extract_data, is_qc_completed, has_suspect
        } ]
    - is_duplicate: 1 if status='DUPLICATE' OR rejected_for IS NOT NULL, else 0
    - has_suspect: 1 if ProcessData.IsSuspectData is not NULL (frontend-friendly)
    """
    q = """
        SELECT
          CONVERT(varchar(10), v.[date], 23)              AS [date],
          v.state                                         AS [state],
          v.state                                         AS [prefix],   -- keep legacy 'prefix'
          v.filename,
          ISNULL(v.pages, 0)                              AS pages,
          CONVERT(varchar(19), v.started, 126)            AS started,    -- yyyy-MM-ddTHH:mm:ss
          CONVERT(varchar(19), v.ended,   126)            AS ended,
          CAST(ISNULL(v.duration_sec, 0.0) AS float)      AS durationSec,
          v.status,
          CONVERT(varchar(36), v.guid)                    AS guid,
          v.run_id,
          v.pdf_md5,
          v.txn_md5,
          v.rejection_reason,
          v.rejected_for,
          CASE WHEN UPPER(ISNULL(v.status, '')) = 'DUPLICATE' OR v.rejected_for IS NOT NULL THEN 1 ELSE 0 END AS is_duplicate,
          pd.IsSuspectData           AS is_suspect_data,
          pd.SuspectExtractData      AS suspect_extract_data,
          pd.IsQCCompleted          AS is_qc_completed,
          CASE WHEN pd.IsSuspectData IS NOT NULL THEN 1 ELSE 0 END AS has_suspect
        FROM dbo.vitu_files v
        LEFT JOIN dbo.ProcessData pd
          ON pd.GUID = v.guid
        WHERE v.[date] BETWEEN ? AND ?
          AND (
              ? IS NULL
              OR v.state = ?
              OR v.filename LIKE ?
          )
        ORDER BY v.[date], v.started, v.filename;
    """
    like = f"{state}-%" if state else None
    with _connect() as cn, cn.cursor() as cur:
        cur.execute(q, (from_date, to_date, state, state, like))
        cols = [c[0] for c in cur.description]
        return _rows_to_dicts(cur, cols)

def get_fields(date: str, filename: str) -> Dict:
    """
    Extracted fields JSON for one file from dbo.vitu_file_outputs.
    """
    q = """
        SELECT fields_json
        FROM dbo.vitu_file_outputs
        WHERE [date] = ? AND filename = ?
    """
    with _connect() as cn, cn.cursor() as cur:
        cur.execute(q, (date, filename))
        row = cur.fetchone()
        if not row or row[0] is None:
            return {}
        try:
            return json.loads(row[0])
        except Exception:
            return {}

def list_audit(from_date: str, to_date: str) -> List[Dict]:
    """
    Per-run aggregates for audit:
      [{ date, run_id, started, ended, received, processed, failed, duplicates }]
    This returns one row per (date, run_id). If run_id is NULL in the DB for some rows,
    they will be grouped under run_id = NULL and still returned (keeps backward compat).
    """
    q = """
        SELECT
          CONVERT(varchar(10), [date], 23)                    AS [date],
          run_id,
          CONVERT(varchar(19), MIN(started), 126)             AS started,
          CONVERT(varchar(19), MAX(ended),   126)             AS ended,
          COUNT(*)                                            AS received,
          SUM(CASE WHEN UPPER(ISNULL(status, '')) = 'SUCCESS'   THEN 1 ELSE 0 END) AS processed,
          SUM(CASE WHEN UPPER(ISNULL(status, '')) = 'FAILED'    THEN 1 ELSE 0 END) AS failed,
          SUM(CASE WHEN UPPER(ISNULL(status, '')) = 'DUPLICATE' THEN 1 ELSE 0 END) AS duplicates
        FROM dbo.vitu_files
        WHERE [date] BETWEEN ? AND ?
        GROUP BY [date], run_id
        ORDER BY [date], started;
    """
    with _connect() as cn, cn.cursor() as cur:
        cur.execute(q, (from_date, to_date))
        cols = [c[0] for c in cur.description]
        return _rows_to_dicts(cur, cols)

def get_s3_key(date: str, filename: str) -> Optional[str]:
    """
    Stored S3 object key for this date+filename (filename should include .pdf).
    """
    q = "SELECT s3_key FROM dbo.vitu_files WHERE [date]=? AND filename=?"
    with _connect() as cn, cn.cursor() as cur:
        cur.execute(q, (date, filename))
        row = cur.fetchone()
        return row[0] if row and row[0] else None

# ----------------------------- writes (used by pipeline & edits) -----------------------------
def upsert_file(row: Dict, s3_key: Optional[str] = None, run_id: Optional[str] = None) -> None:
    """
    Upsert one processing summary row into dbo.vitu_files.

    Expected keys in row:
      date (YYYY-MM-DD), filename, pages, started, ended, durationSec, status
    Accepts either row['state'] or row['prefix'] (will map to state).

    New optional parameter:
      run_id: string identifier for this pipeline run (stored in run_id column).
    Note: GUID is generated in DB by default constraint; we don't set it here.
    """
    date     = row.get("date")
    state    = row.get("state") or row.get("prefix")
    filename = row.get("filename")
    pages    = row.get("pages")
    started  = row.get("started")
    ended    = row.get("ended")
    duration = row.get("durationSec")
    status   = row.get("status")

    # New optional fields
    pdf_md5 = row.get("pdf_md5")
    txn_md5 = row.get("txn_md5")
    rejection_reason = row.get("rejection_reason")
    rejected_for = row.get("rejected_for")

    q = """
    SET NOCOUNT ON;
    MERGE dbo.vitu_files AS t
    USING (SELECT ? AS [date], ? AS filename) AS s
    ON (t.[date] = s.[date] AND t.filename = s.filename)
    WHEN MATCHED THEN UPDATE SET
        state = ?, pages = ?, started = ?, ended = ?, duration_sec = ?, status = ?,
        s3_key = COALESCE(?, t.s3_key),
        pdf_md5 = COALESCE(?, t.pdf_md5),
        txn_md5 = COALESCE(?, t.txn_md5),
        rejection_reason = COALESCE(?, t.rejection_reason),
        rejected_for = COALESCE(?, t.rejected_for),
        run_id = COALESCE(?, t.run_id)
    WHEN NOT MATCHED THEN INSERT
        ([date], state, filename, pages, started, ended, duration_sec, status, s3_key, pdf_md5, txn_md5, rejection_reason, rejected_for, run_id)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
    """
    params = (
        # MATCH params
        date, filename,
        state, pages, started, ended, duration, status, s3_key,
        pdf_md5, txn_md5, rejection_reason, rejected_for, run_id,
        # INSERT params (same order as VALUES)
        date, state, filename, pages, started, ended, duration, status, s3_key, pdf_md5, txn_md5, rejection_reason, rejected_for, run_id
    )

    with _connect() as cn, cn.cursor() as cur:
        try:
            cur.execute(q, params)
        except pyodbc.Error as e:
            msg = str(e)
            # Some ODBC drivers produce non-fatal informational error text for non-query operations.
            if "No results" in msg or "Previous SQL was not a query" in msg:
                print(f"[WARN] upsert_file driver notice (ignored): {msg}")
            else:
                raise

def upsert_output(date: str, filename: str, fields: Dict) -> None:
    """
    Upsert extracted fields JSON into dbo.vitu_file_outputs.
    Used by the pipeline and by the /api/file/update (View page Save).
    """
    payload = json.dumps(fields, ensure_ascii=False)
    q = """
    SET NOCOUNT ON;
    MERGE dbo.vitu_file_outputs AS t
    USING (SELECT ? AS [date], ? AS filename) AS s
    ON (t.[date] = s.[date] AND t.filename = s.filename)
    WHEN MATCHED THEN UPDATE SET fields_json = ?
    WHEN NOT MATCHED THEN INSERT ([date], filename, fields_json) VALUES (?, ?, ?);
    """
    params = (date, filename, payload, date, filename, payload)
    with _connect() as cn, cn.cursor() as cur:
        try:
            cur.execute(q, params)
        except pyodbc.Error as e:
            msg = str(e)
            if "No results" in msg or "Previous SQL was not a query" in msg:
                print(f"[WARN] upsert_output driver notice (ignored): {msg}")
            else:
                raise

# ----------------------------- md5 / duplicate helpers -----------------------------
def find_by_md5(pdf_md5: Optional[str], txn_md5: Optional[str]) -> List[Dict]:
    """
    Return rows matching either pdf_md5 or txn_md5.
    Pass None for unused values.
    """
    # defensive: if both None/empty, nothing to search
    if not pdf_md5 and not txn_md5:
        return []

    q = """
    SELECT [date], filename, state, pages, started, ended, duration_sec AS durationSec,
           status, pdf_md5, txn_md5, rejection_reason, rejected_for, s3_key
    FROM dbo.vitu_files
    WHERE (? IS NOT NULL AND pdf_md5 = ?)
       OR (? IS NOT NULL AND txn_md5 = ?)
    """
    with _connect() as cn, cn.cursor() as cur:
        cur.execute(q, (pdf_md5, pdf_md5, txn_md5, txn_md5))
        cols = [c[0] for c in cur.description]
        return _rows_to_dicts(cur, cols)

def list_rejected(date: str, run_id: Optional[str] = None) -> List[Dict]:
    """
    Return rows for a given date that are failed, duplicate, or have rejection_reason.
    If run_id is provided, return only rows for that run_id.
    Used by the audit UI to show rejected details.
    """
    if run_id:
        q = """
        SELECT filename, state, pages, CONVERT(varchar(19), started, 126) AS started,
               CONVERT(varchar(19), ended,   126) AS ended, duration_sec AS durationSec,
               status, pdf_md5, txn_md5, rejection_reason, rejected_for, s3_key, run_id
        FROM dbo.vitu_files
        WHERE [date] = ? AND run_id = ? AND (
            status IN ('FAILED', 'DUPLICATE') OR rejection_reason IS NOT NULL OR rejected_for IS NOT NULL
        )
        ORDER BY started;
        """
        params = (date, run_id)
    else:
        q = """
        SELECT filename, state, pages, CONVERT(varchar(19), started, 126) AS started,
               CONVERT(varchar(19), ended,   126) AS ended, duration_sec AS durationSec,
               status, pdf_md5, txn_md5, rejection_reason, rejected_for, s3_key, run_id
        FROM dbo.vitu_files
        WHERE [date] = ? AND (
            status IN ('FAILED', 'DUPLICATE') OR rejection_reason IS NOT NULL OR rejected_for IS NOT NULL
        )
        ORDER BY started;
        """
        params = (date,)

    with _connect() as cn, cn.cursor() as cur:
        cur.execute(q, params)
        cols = [c[0] for c in cur.description]
        return _rows_to_dicts(cur, cols)

# ----------------------------- run / run-scoped helpers -----------------------------
def list_runs(from_date: str, to_date: str) -> List[Dict]:
    """
    Return per-run aggregates for audit:
      [{ date, run_id, started, ended, received, processed, failed }]
    """
    q = """
    SELECT
      CONVERT(varchar(10), [date], 23)              AS [date],
      run_id,
      CONVERT(varchar(19), MIN(started), 126)       AS started,
      CONVERT(varchar(19), MAX(ended),   126)       AS ended,
      COUNT(*)                                      AS received,
      SUM(CASE WHEN status='SUCCESS' THEN 1 ELSE 0 END) AS processed,
      SUM(CASE WHEN status='FAILED'  THEN 1 ELSE 0 END) AS failed
    FROM dbo.vitu_files
    WHERE [date] BETWEEN ? AND ?
      AND run_id IS NOT NULL
    GROUP BY [date], run_id
    ORDER BY [date], started;
    """
    with _connect() as cn, cn.cursor() as cur:
        cur.execute(q, (from_date, to_date))
        cols = [c[0] for c in cur.description]
        return _rows_to_dicts(cur, cols)

def get_files_by_run(run_id: str) -> List[Dict]:
    """
    Return file rows for a given run_id. Includes metadata used by UI and download.
    """
    q = """
    SELECT
      CONVERT(varchar(10), [date], 23)              AS [date],
      state,
      filename,
      ISNULL(pages, 0)                              AS pages,
      CONVERT(varchar(19), started, 126)            AS started,
      CONVERT(varchar(19), ended,   126)            AS ended,
      CAST(ISNULL(duration_sec, 0.0) AS float)      AS durationSec,
      status,
      CONVERT(varchar(36), guid)                    AS guid,
      pdf_md5,
      txn_md5,
      rejection_reason,
      rejected_for,
      s3_key,
      run_id
    FROM dbo.vitu_files
    WHERE run_id = ?
    ORDER BY started, filename;
    """
    with _connect() as cn, cn.cursor() as cur:
        cur.execute(q, (run_id,))
        cols = [c[0] for c in cur.description]
        return _rows_to_dicts(cur, cols)

def get_processed_files_by_run(run_id: str) -> List[Dict]:
    """
    Return only processed (status = 'SUCCESS') file rows for a given run_id.
    Useful for download endpoints that must include only processed files for that run.
    """
    q = """
    SELECT
      CONVERT(varchar(10), [date], 23)              AS [date],
      state,
      filename,
      ISNULL(pages, 0)                              AS pages,
      CONVERT(varchar(19), started, 126)            AS started,
      CONVERT(varchar(19), ended,   126)            AS ended,
      CAST(ISNULL(duration_sec, 0.0) AS float)      AS durationSec,
      status,
      CONVERT(varchar(36), guid)                    AS guid,
      pdf_md5,
      txn_md5,
      rejection_reason,
      rejected_for,
      s3_key,
      run_id
    FROM dbo.vitu_files
    WHERE run_id = ? AND UPPER(ISNULL(status, '')) = 'SUCCESS'
    ORDER BY started, filename;
    """
    with _connect() as cn, cn.cursor() as cur:
        cur.execute(q, (run_id,))
        cols = [c[0] for c in cur.description]
        return _rows_to_dicts(cur, cols)

# ----------------------------- ProcessData helpers (NEW) -----------------------------
def get_guid_by_date_filename(date: str, filename: str) -> Optional[str]:
    """
    Return GUID (varchar) from dbo.vitu_files matching the given date and filename.
    Returns None if not found.
    """
    q = "SELECT CONVERT(varchar(36), guid) FROM dbo.vitu_files WHERE [date] = ? AND filename = ?"
    with _connect() as cn, cn.cursor() as cur:
        cur.execute(q, (date, filename))
        row = cur.fetchone()
        return row[0] if row and row[0] else None

def get_processdata_by_guid(guid: str) -> Optional[Dict]:
    """
    Fetch the ProcessData row by GUID. Returns a dict mapping column->value or None.
    """
    q = "SELECT * FROM dbo.ProcessData WHERE GUID = ?"
    with _connect() as cn, cn.cursor() as cur:
        cur.execute(q, (guid,))
        row = cur.fetchone()
        if not row:
            return None
        cols = [c[0] for c in cur.description]
        return dict(zip(cols, row))

def get_processdata_by_date_filename(date: str, filename: str) -> Optional[Dict]:
    """
    Convenience: find GUID by date+filename then return ProcessData row (or None).
    """
    guid = get_guid_by_date_filename(date, filename)
    if not guid:
        return None
    return get_processdata_by_guid(guid)

def get_processdata_flags_by_date_filename(date: str, filename: str) -> Dict:
    """
    Convenience wrapper that returns parsed flags for the given file:
      {
        "has_suspect": bool,
        "suspect_map": Dict[str,str] or {},
        "is_qc_completed": bool,
        "qc_corrected": Dict or None
      }
    Decodes JSON columns where appropriate.
    """
    out = {"has_suspect": False, "suspect_map": {}, "is_qc_completed": False, "qc_corrected": None}
    proc = get_processdata_by_date_filename(date, filename)
    if not proc:
        return out
    # decode IsSuspectData / SuspectExtractData / IsQCCompleted / QCCorrectedData
    try:
        is_sus = proc.get("IsSuspectData")
        if is_sus:
            try:
                m = json.loads(is_sus) if isinstance(is_sus, str) else dict(is_sus)
                out["suspect_map"] = m
                out["has_suspect"] = bool(m)
            except Exception:
                out["suspect_map"] = {}
                out["has_suspect"] = True
        sus_vals = proc.get("SuspectExtractData")
        if sus_vals:
            try:
                v = json.loads(sus_vals) if isinstance(sus_vals, str) else dict(sus_vals)
                # keep suspect_map and include values under same key when available
                out["suspect_values"] = v
            except Exception:
                out["suspect_values"] = {}
        is_qc = proc.get("IsQCCompleted")
        if isinstance(is_qc, str):
            out["is_qc_completed"] = is_qc.upper() == "YES"
        elif isinstance(is_qc, (int, bool)):
            out["is_qc_completed"] = bool(is_qc)
        qc_corr = proc.get("QCCorrectedData")
        if qc_corr:
            try:
                out["qc_corrected"] = json.loads(qc_corr) if isinstance(qc_corr, str) else qc_corr
            except Exception:
                out["qc_corrected"] = qc_corr
    except Exception:
        pass
    return out

def upsert_processdata(process_row: Dict) -> Optional[str]:
    """
    Upsert row into dbo.ProcessData. Expects a dictionary 'process_row' containing keys:
      GUID (optional), FileName, State_Name, DataExtractedDateTime, SuspectCheckingDateTime,
      IsDataExtracted, IsSuspectValidated, IsQCCompleted, IsDelivered,
      AWSS3Path, ExtractedData, IsSuspectData, SuspectExtractData, QCCorrectedData, QCCheckingDateTime

    All text/JSON fields should be passed as Python strings (json.dumps where appropriate).

    Returns:
      GUID (str) used to upsert the row (incoming GUID if provided; newly generated GUID when None)
    """
    # We'll use SQL MERGE with ISNULL(?, NEWID()) to ensure a GUID exists, and use OUTPUT to return it.
    q = """
    SET NOCOUNT ON;
    DECLARE @out TABLE (GUID uniqueidentifier);

    MERGE dbo.ProcessData AS target
    USING (SELECT ISNULL(? , NEWID()) AS GUID) AS src
    ON (target.GUID = src.GUID)
    WHEN MATCHED THEN
      UPDATE SET
        FileName = ?,
        State_Name = ?,
        DataExtractedDateTime = ?,
        SuspectCheckingDateTime = ?,
        IsDataExtracted = ?,
        IsSuspectValidated = ?,
        IsQCCompleted = ?,
        IsDelivered = ?,
        AWSS3Path = ?,
        ExtractedData = ?,
        IsSuspectData = ?,
        SuspectExtractData = ?,
        QCCorrectedData = COALESCE(?, QCCorrectedData),
        QCCheckingDateTime = COALESCE(?, QCCheckingDateTime)
    WHEN NOT MATCHED THEN
      INSERT (GUID, FileName, State_Name, UploadDatetime, DataExtractedDateTime, SuspectCheckingDateTime, IsDataExtracted, IsSuspectValidated, IsQCCompleted, IsDelivered, AWSS3Path, ExtractedData, IsSuspectData, SuspectExtractData, QCCorrectedData, QCCheckingDateTime)
      VALUES (src.GUID, ?, ?, GETDATE(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    OUTPUT inserted.GUID INTO @out;

    SELECT CONVERT(varchar(36), GUID) AS GUID FROM @out;
    """

    # Read values with safe defaults
    guid = process_row.get("GUID")  # may be None
    filename = process_row.get("FileName")
    state_name = process_row.get("State_Name")
    data_extracted_dt = process_row.get("DataExtractedDateTime")
    suspect_check_dt = process_row.get("SuspectCheckingDateTime")
    is_data_extracted = process_row.get("IsDataExtracted", "No")
    is_suspect_validated = process_row.get("IsSuspectValidated", "No")
    is_qc_completed = process_row.get("IsQCCompleted", "No")
    is_delivered = 1 if process_row.get("IsDelivered") else 0
    aws_s3_path = process_row.get("AWSS3Path")
    extracted_data = process_row.get("ExtractedData")
    is_suspect_data = process_row.get("IsSuspectData")
    suspect_extract_data = process_row.get("SuspectExtractData")
    qc_corrected = process_row.get("QCCorrectedData")
    qc_checking_dt = process_row.get("QCCheckingDateTime")

    # params correspond to ? placeholders in SQL above (order matters)
    params = (
        # ISNULL(? , NEWID()) -> match key
        guid,
        # UPDATE params (for WHEN MATCHED -> UPDATE)
        filename, state_name, data_extracted_dt, suspect_check_dt, is_data_extracted,
        is_suspect_validated, is_qc_completed, is_delivered, aws_s3_path, extracted_data, is_suspect_data,
        suspect_extract_data, qc_corrected, qc_checking_dt,
        # INSERT params (for WHEN NOT MATCHED -> INSERT, note src.GUID used for GUID)
        filename, state_name, data_extracted_dt, suspect_check_dt, is_data_extracted,
        is_suspect_validated, is_qc_completed, is_delivered, aws_s3_path, extracted_data, is_suspect_data,
        suspect_extract_data, qc_corrected, qc_checking_dt
    )

    with _connect() as cn, cn.cursor() as cur:
        try:
            cur.execute(q, params)
        except pyodbc.Error as e:
            msg = str(e)
            if "No results" in msg or "Previous SQL was not a query" in msg:
                # informational driver message; proceed to try to fetch any returned row
                print(f"[WARN] upsert_processdata driver notice (ignored): {msg}")
            else:
                raise

        # Attempt to fetch GUID if produced using robust helper
        row = _fetch_first_row_from_cursor(cur)
        if row and row[0]:
            return str(row[0])

    return None

# ----------------------------- existence helper -----------------------------
def file_exists_success(date: str, filename: str) -> bool:
    """
    Check if a file with this date+filename already exists in DB with status = 'SUCCESS'.
    Used by main_db.py to skip reprocessing already successful files.
    """
    q = """
    SELECT COUNT(*) 
    FROM dbo.vitu_files
    WHERE [date] = ? AND filename = ? AND UPPER(status) = 'SUCCESS'
    """
    with _connect() as cn, cn.cursor() as cur:
        cur.execute(q, (date, filename))
        row = cur.fetchone()
        return (row[0] > 0) if row else False
