"""JSON parser for dlist"""

import glob
import json
import os
from pathlib import Path
from .base import BaseParser


class JSONParser(BaseParser):
    """Parse JSON to Dlist

    Accepts a file path, a JSON string, or a Python list of dicts.

    Examples::

        >>> from dlist.parsers import get_parser
        >>> parser = get_parser('json')
        >>> d = parser.parse('data.json', id_='id')
        >>> d = parser.parse('[{"name":"John"}]')
        >>> d = parser.parse([{"name":"John"}])
    """

    def parse(self, source, id_='', **kwargs):
        """Parse JSON source into Dlist

        Parameters:
            source: JSON string, file path, or list of dicts
            id_ (str): Optional id field name

        Returns:
            Dlist: Parsed Dlist object
        """
        # Import here to avoid circular dependency
        from ..dlist import Dlist

        if isinstance(source, list):
            data = source
        elif isinstance(source, str):
            if os.path.isfile(source):
                with open(source, 'r', encoding='utf-8') as f:
                    data = json.load(f)
            else:
                try:
                    data = json.loads(source)
                except json.JSONDecodeError as e:
                    raise ValueError(f"Invalid JSON string: {e}")
        else:
            raise ValueError(f"Unsupported source type: {type(source)}")

        if not isinstance(data, list):
            raise ValueError("JSON must contain a list of dictionaries")

        # Merge duplicates when id_ is specified
        if id_ and data:
            merged = {}  # id_value → record
            order = []   # preserve first-seen order
            for rec in data:
                id_val = rec.get(id_, '')
                if id_val not in merged:
                    merged[id_val] = dict(rec)
                    order.append(id_val)
                else:
                    # First wins: only add missing fields
                    for k, v in rec.items():
                        if k not in merged[id_val]:
                            merged[id_val][k] = v
            data = [merged[id_val] for id_val in order]

        return Dlist(data, id_=id_)

    @staticmethod
    def _glob_to_files(pattern):
        """Expand a glob pattern into a ``{path: key_name}`` dict.

        Key names are derived from the file stem with a trailing ``'s'``
        stripped:  ``types.json`` → ``'type'``, ``exifs.json`` → ``'exif'``.
        """
        paths = sorted(glob.glob(pattern))
        if not paths:
            raise FileNotFoundError(
                f"No files matched the pattern: {pattern}")
        files = {}
        for p in paths:
            stem = Path(p).stem
            key = stem.rstrip('s') if stem.endswith('s') and len(stem) > 1 else stem
            files[p] = key
        return files

    def parse_pivot(self, files, pivot='id', base=None):
        """Join multiple JSON files by a shared pivot key using DuckDB

        Each file is a JSON array of objects sharing a common ``pivot``
        field (e.g. ``"id"``).  The non-pivot columns of every file are
        merged into a single Dlist record via SQL LEFT JOIN.

        Parameters:
            files (dict|str): Either a ``{filepath: key_name}`` mapping,
                or a **glob pattern** string (e.g. ``'data/*.json'``).

                When a glob is given, key names are derived automatically
                from each file's stem with a trailing ``'s'`` stripped::

                    files.json  → 'file'
                    types.json  → 'type'
                    exifs.json  → 'exif'

                When a dict, the *value* tells the loader under which key
                to nest that file's data in the final record.

                * If the file has a **single** non-pivot column that is a
                  dict, it is nested under the given key name.
                * If the file has a **single** non-pivot column that is a
                  scalar, its value is stored directly under the key name.
                * If the file has **multiple** non-pivot columns, they are
                  grouped into a sub-dict under the key name.

            pivot (str): The join key present in every file (default: ``'id'``).
            base (str|None): File path that drives the result set (LEFT JOIN
                base).  Records only in non-base files are dropped.
                If ``None``, the first file in *files* is used.

        Returns:
            Dlist: Merged Dlist with ``id_=pivot``

        Raises:
            ImportError: If ``duckdb`` is not installed.
            FileNotFoundError: If a file path does not exist or glob matches nothing.

        .. warning::

            Values are preserved as their native JSON types (str, int,
            float, bool, None).  ``filter()`` comparisons are strict:
            ``30`` does not match ``'30'``.  Negative numbers and
            booleans may conflict with the ``-key`` / ``!key`` filter
            syntax — use programmatic filtering for those cases.

        Examples::

            >>> d = parser.parse_pivot('data/*.json', pivot='id')

            >>> d = parser.parse_pivot({
            ...     'files.json':   'file',
            ...     'types.json':   'type',
            ...     'exifs.json':   'exif',
            ... }, pivot='id')

            >>> d = Dlist.read_pivot('evidence/*.json')
        """
        # Accept glob pattern string → expand to {path: key_name} dict
        if isinstance(files, str):
            files = self._glob_to_files(files)

        try:
            import duckdb
        except ImportError:
            raise ImportError(
                "duckdb is required for read_pivot. "
                "Install it with: pip install duckdb"
            )
        from ..dlist import Dlist

        if not files:
            raise ValueError("files dict must not be empty")

        # Validate all files exist
        for fpath in files:
            if not os.path.isfile(fpath):
                raise FileNotFoundError(f"File not found: {fpath}")

        # Resolve base file (first in dict if not specified)
        file_list = list(files.items())  # [(path, key_name), ...]
        if base is not None:
            # Move base to front
            base_idx = None
            for i, (fp, _) in enumerate(file_list):
                if fp == base:
                    base_idx = i
                    break
            if base_idx is None:
                raise ValueError(f"base file '{base}' not found in files dict")
            file_list.insert(0, file_list.pop(base_idx))

        con = duckdb.connect()

        # ── Build table expressions ──────────────────────────────────
        # For each file: sample first record to detect column types,
        # read dicts as JSON blobs, scalars as VARCHAR.
        tables = []  # [(sql_expr, alias, key_name, [(col, type)])]
        for idx, (fpath, key_name) in enumerate(file_list):
            with open(fpath, 'r', encoding='utf-8') as fp:
                sample = json.load(fp)[0]

            alias = f't{idx}'
            other_keys = [k for k in sample.keys() if k != pivot]

            col_spec = f'"{pivot}": "VARCHAR"'
            col_info = []
            for k in other_keys:
                if isinstance(sample[k], dict):
                    col_spec += f', "{k}": "JSON"'
                    col_info.append((k, 'json'))
                else:
                    col_spec += f', "{k}": "JSON"'
                    col_info.append((k, 'scalar_json'))

            expr = f"read_json('{fpath}', format='array', columns={{{col_spec}}})"
            tables.append((expr, alias, key_name, col_info))

        # ── Build SQL ────────────────────────────────────────────────
        select_parts = [f'{tables[0][1]}.{pivot}']
        for _expr, alias, _key_name, cols in tables:
            for col_name, _col_type in cols:
                select_parts.append(f'{alias}."{col_name}"')

        from_clause = f'{tables[0][0]} {tables[0][1]}'
        join_clauses = ''
        for expr, alias, _key_name, _cols in tables[1:]:
            join_clauses += f' LEFT JOIN {expr} {alias} USING ("{pivot}")'

        sql = f'SELECT {", ".join(select_parts)} FROM {from_clause}{join_clauses}'
        rows = con.execute(sql).fetchall()

        # ── Build column metadata ────────────────────────────────────
        col_meta = [(pivot, None, 'pivot')]  # (col_name, key_name, type)
        for _expr, _alias, key_name, cols in tables:
            for col_name, col_type in cols:
                col_meta.append((col_name, key_name, col_type))

        # ── Determine nesting strategy per file ──────────────────────
        # If a file has a single column whose name matches key_name → direct
        # If a file has a single column with a different name → nest under key_name
        # If a file has multiple columns → group under key_name as sub-dict
        file_cols = {}  # key_name → [(col_name, col_type)]
        for _expr, _alias, key_name, cols in tables:
            file_cols[key_name] = cols

        # ── Convert rows to dicts ────────────────────────────────────
        data = []
        for row in rows:
            d = {}
            # First value is the pivot
            d[pivot] = row[0]

            val_idx = 1
            for _expr, _alias, key_name, cols in tables:
                if len(cols) == 1:
                    col_name, col_type = cols[0]
                    val = row[val_idx]
                    val_idx += 1
                    if val is None:
                        continue
                    parsed = json.loads(val)
                    if col_name == key_name:
                        # Direct: file has {pivot, key_name} → store as d[key_name]
                        d[key_name] = parsed
                    else:
                        # Single col but name differs → nest under key_name
                        d[key_name] = parsed
                else:
                    # Multiple columns → group as sub-dict
                    sub = {}
                    for col_name, col_type in cols:
                        val = row[val_idx]
                        val_idx += 1
                        if val is None:
                            continue
                        sub[col_name] = json.loads(val)
                    if sub:
                        d[key_name] = sub

            data.append(d)

        con.close()
        return Dlist(data, id_=pivot)
