# Pick-Style Console System - Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Build a keyboard-driven Pick/VT220-inspired console application for the autoparts catalog with two rendering modes (curses and textual).

**Architecture:** Three-layer architecture — DB layer (SQLite, swappable to PostgreSQL), business logic screens, and pluggable renderers (curses for VT220 aesthetic, textual for modern TUI). All screens define data/actions abstractly; renderers handle drawing.

**Tech Stack:** Python 3, curses (stdlib), textual/rich (pip), sqlite3 (stdlib), requests (for NHTSA VIN API)

---

## Task 1: Project Scaffold and Config

**Files:**
- Create: `console/__init__.py`
- Create: `console/main.py`
- Create: `console/config.py`
- Create: `console/core/__init__.py`
- Create: `console/screens/__init__.py`
- Create: `console/renderers/__init__.py`
- Create: `console/utils/__init__.py`

**Step 1: Create directory structure**

```bash
mkdir -p /home/Autopartes/console/{core,screens,renderers,utils}
touch /home/Autopartes/console/__init__.py
touch /home/Autopartes/console/core/__init__.py
touch /home/Autopartes/console/screens/__init__.py
touch /home/Autopartes/console/renderers/__init__.py
touch /home/Autopartes/console/utils/__init__.py
```

**Step 2: Write config.py**

```python
# console/config.py
import os

VERSION = "1.0.0"
APP_NAME = "AUTOPARTES"
APP_SUBTITLE = "Sistema de Catálogo de Autopartes"

# Database
DB_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'vehicle_database', 'vehicle_database.db')

# VIN API
NHTSA_API_URL = "https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVin"
VIN_CACHE_DAYS = 30

# Display
DEFAULT_MODE = "vt220"  # "vt220" or "modern"
PAGE_SIZE = 15  # rows per page in lists/tables

# Color schemes for curses (name: (fg, bg))
COLORS_VT220 = {
    'header': ('green', 'black'),
    'footer': ('black', 'green'),
    'normal': ('green', 'black'),
    'highlight': ('black', 'green'),
    'border': ('green', 'black'),
    'title': ('white', 'black'),
    'error': ('red', 'black'),
    'info': ('cyan', 'black'),
    'field_label': ('green', 'black'),
    'field_value': ('white', 'black'),
    'field_active': ('black', 'cyan'),
}
```

**Step 3: Write main.py entry point**

```python
# console/main.py
import argparse
import sys
from console.config import VERSION, DEFAULT_MODE


def parse_args():
    parser = argparse.ArgumentParser(description='Autopartes Console - Pick-Style Catalog System')
    parser.add_argument('--mode', choices=['vt220', 'modern'], default=DEFAULT_MODE,
                        help='Rendering mode: vt220 (curses) or modern (textual)')
    parser.add_argument('--version', action='version', version=f'Autopartes Console v{VERSION}')
    return parser.parse_args()


def main():
    args = parse_args()

    if args.mode == 'vt220':
        from console.renderers.curses_renderer import CursesRenderer
        renderer = CursesRenderer()
    else:
        from console.renderers.textual_renderer import TextualRenderer
        renderer = TextualRenderer()

    from console.db import Database
    from console.core.app import App

    db = Database()
    app = App(renderer, db)
    app.run()


if __name__ == '__main__':
    main()
```

**Step 4: Commit**

```bash
git add console/
git commit -m "feat(console): scaffold project structure and config"
```

---

## Task 2: Database Abstraction Layer

**Files:**
- Create: `console/db.py`
- Create: `console/tests/__init__.py`
- Create: `console/tests/test_db.py`

**Step 1: Write test for DB layer**

```python
# console/tests/test_db.py
import os
import sys
import pytest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
from console.db import Database


@pytest.fixture
def db():
    return Database()


def test_get_brands(db):
    brands = db.get_brands()
    assert isinstance(brands, list)
    assert len(brands) > 0
    assert 'name' in brands[0]


def test_get_models_no_filter(db):
    models = db.get_models()
    assert isinstance(models, list)
    assert len(models) > 0


def test_get_models_by_brand(db):
    models = db.get_models(brand='TOYOTA')
    assert isinstance(models, list)
    assert len(models) > 0


def test_get_categories(db):
    cats = db.get_categories()
    assert isinstance(cats, list)
    assert len(cats) == 12


def test_get_groups(db):
    groups = db.get_groups(category_id=2)
    assert isinstance(groups, list)
    assert len(groups) > 0
    assert 'name' in groups[0]


def test_get_parts_by_group(db):
    parts = db.get_parts(group_id=17)
    assert isinstance(parts, list)


def test_get_part_detail(db):
    part = db.get_part(1)
    assert part is not None
    assert 'oem_part_number' in part


def test_get_alternatives(db):
    alts = db.get_alternatives(1)
    assert isinstance(alts, list)


def test_get_cross_references(db):
    refs = db.get_cross_references(1)
    assert isinstance(refs, list)


def test_search_parts_fts(db):
    results = db.search_parts('brake')
    assert isinstance(results, list)
    assert len(results) > 0


def test_search_part_number(db):
    results = db.search_part_number('04465')
    assert isinstance(results, list)


def test_get_stats(db):
    stats = db.get_stats()
    assert 'brands' in stats
    assert 'models' in stats
    assert 'parts' in stats
    assert stats['brands'] > 0


def test_get_years(db):
    years = db.get_years(brand='TOYOTA', model='CAMRY')
    assert isinstance(years, list)


def test_get_engines(db):
    engines = db.get_engines(brand='TOYOTA', model='CAMRY')
    assert isinstance(engines, list)
```

**Step 2: Run test to verify it fails**

```bash
cd /home/Autopartes && python -m pytest console/tests/test_db.py -v
```

Expected: FAIL — `ModuleNotFoundError: No module named 'console.db'`

**Step 3: Implement db.py**

```python
# console/db.py
import sqlite3
import os
from console.config import DB_PATH


class Database:
    def __init__(self, db_path=None):
        self.db_path = db_path or DB_PATH

    def _connect(self):
        conn = sqlite3.connect(self.db_path)
        conn.row_factory = sqlite3.Row
        return conn

    def _query(self, sql, params=(), one=False):
        conn = self._connect()
        cursor = conn.cursor()
        cursor.execute(sql, params)
        rows = cursor.fetchall()
        conn.close()
        if one:
            return dict(rows[0]) if rows else None
        return [dict(r) for r in rows]

    def _execute(self, sql, params=()):
        conn = self._connect()
        cursor = conn.cursor()
        cursor.execute(sql, params)
        conn.commit()
        last_id = cursor.lastrowid
        conn.close()
        return last_id

    # === Vehicle Navigation ===

    def get_brands(self):
        return self._query("""
            SELECT id, name, country FROM brands ORDER BY name
        """)

    def get_models(self, brand=None):
        if brand:
            return self._query("""
                SELECT DISTINCT m.id, m.name
                FROM models m
                JOIN brands b ON m.brand_id = b.id
                WHERE UPPER(b.name) = UPPER(?)
                ORDER BY m.name
            """, (brand,))
        return self._query("SELECT id, name FROM models ORDER BY name")

    def get_years(self, brand=None, model=None):
        sql = """
            SELECT DISTINCT y.id, y.year
            FROM years y
            JOIN model_year_engine mye ON mye.year_id = y.id
            JOIN models m ON mye.model_id = m.id
            JOIN brands b ON m.brand_id = b.id
            WHERE 1=1
        """
        params = []
        if brand:
            sql += " AND UPPER(b.name) = UPPER(?)"
            params.append(brand)
        if model:
            sql += " AND UPPER(m.name) = UPPER(?)"
            params.append(model)
        sql += " ORDER BY y.year DESC"
        return self._query(sql, params)

    def get_engines(self, brand=None, model=None, year=None):
        sql = """
            SELECT DISTINCT e.id, e.name, e.displacement_cc, e.cylinders,
                   e.fuel_type, e.power_hp, e.torque_nm
            FROM engines e
            JOIN model_year_engine mye ON mye.engine_id = e.id
            JOIN models m ON mye.model_id = m.id
            JOIN brands b ON m.brand_id = b.id
            WHERE 1=1
        """
        params = []
        if brand:
            sql += " AND UPPER(b.name) = UPPER(?)"
            params.append(brand)
        if model:
            sql += " AND UPPER(m.name) = UPPER(?)"
            params.append(model)
        if year:
            sql += " AND mye.year_id IN (SELECT id FROM years WHERE year = ?)"
            params.append(int(year))
        sql += " ORDER BY e.name"
        return self._query(sql, params)

    def get_model_year_engine(self, brand, model, year, engine_id):
        return self._query("""
            SELECT mye.id, mye.trim_level, mye.drivetrain, mye.transmission
            FROM model_year_engine mye
            JOIN models m ON mye.model_id = m.id
            JOIN brands b ON m.brand_id = b.id
            JOIN years y ON mye.year_id = y.id
            WHERE UPPER(b.name) = UPPER(?)
              AND UPPER(m.name) = UPPER(?)
              AND y.year = ?
              AND mye.engine_id = ?
        """, (brand, model, int(year), engine_id))

    # === Parts Catalog ===

    def get_categories(self):
        return self._query("""
            SELECT id, name, name_es, icon_name, slug, display_order
            FROM part_categories ORDER BY display_order
        """)

    def get_groups(self, category_id):
        return self._query("""
            SELECT id, name, name_es, slug, display_order
            FROM part_groups WHERE category_id = ? ORDER BY display_order
        """, (category_id,))

    def get_parts(self, group_id=None, mye_id=None, page=1, per_page=15):
        offset = (page - 1) * per_page
        if mye_id and group_id:
            return self._query("""
                SELECT p.id, p.oem_part_number, p.name, p.name_es,
                       pg.name AS group_name, vp.quantity_required, vp.position,
                       (SELECT COUNT(*) FROM aftermarket_parts ap WHERE ap.oem_part_id = p.id) AS alt_count
                FROM parts p
                JOIN vehicle_parts vp ON vp.part_id = p.id
                JOIN part_groups pg ON p.group_id = pg.id
                WHERE vp.model_year_engine_id = ? AND p.group_id = ?
                ORDER BY p.name
                LIMIT ? OFFSET ?
            """, (mye_id, group_id, per_page, offset))
        elif mye_id:
            return self._query("""
                SELECT p.id, p.oem_part_number, p.name, p.name_es,
                       pg.name AS group_name, vp.quantity_required, vp.position,
                       (SELECT COUNT(*) FROM aftermarket_parts ap WHERE ap.oem_part_id = p.id) AS alt_count
                FROM parts p
                JOIN vehicle_parts vp ON vp.part_id = p.id
                JOIN part_groups pg ON p.group_id = pg.id
                WHERE vp.model_year_engine_id = ?
                ORDER BY pg.display_order, p.name
                LIMIT ? OFFSET ?
            """, (mye_id, per_page, offset))
        elif group_id:
            return self._query("""
                SELECT p.id, p.oem_part_number, p.name, p.name_es,
                       pg.name AS group_name,
                       (SELECT COUNT(*) FROM aftermarket_parts ap WHERE ap.oem_part_id = p.id) AS alt_count
                FROM parts p
                JOIN part_groups pg ON p.group_id = pg.id
                WHERE p.group_id = ?
                ORDER BY p.name
                LIMIT ? OFFSET ?
            """, (group_id, per_page, offset))
        return self._query("""
            SELECT p.id, p.oem_part_number, p.name, p.name_es,
                   pg.name AS group_name,
                   (SELECT COUNT(*) FROM aftermarket_parts ap WHERE ap.oem_part_id = p.id) AS alt_count
            FROM parts p
            JOIN part_groups pg ON p.group_id = pg.id
            ORDER BY p.name
            LIMIT ? OFFSET ?
        """, (per_page, offset))

    def get_part(self, part_id):
        return self._query("""
            SELECT p.*, pg.name AS group_name, pg.category_id,
                   pc.name AS category_name, pc.name_es AS category_name_es
            FROM parts p
            JOIN part_groups pg ON p.group_id = pg.id
            JOIN part_categories pc ON pg.category_id = pc.id
            WHERE p.id = ?
        """, (part_id,), one=True)

    def get_alternatives(self, part_id):
        return self._query("""
            SELECT ap.*, m.name AS manufacturer_name, m.country AS manufacturer_country
            FROM aftermarket_parts ap
            JOIN manufacturers m ON ap.manufacturer_id = m.id
            WHERE ap.oem_part_id = ?
            ORDER BY
                CASE ap.quality_tier
                    WHEN 'premium' THEN 1
                    WHEN 'standard' THEN 2
                    WHEN 'economy' THEN 3
                    ELSE 4
                END
        """, (part_id,))

    def get_cross_references(self, part_id):
        return self._query("""
            SELECT * FROM part_cross_references
            WHERE part_id = ? ORDER BY reference_type
        """, (part_id,))

    # === Search ===

    def search_parts(self, query, page=1, per_page=15):
        offset = (page - 1) * per_page
        return self._query("""
            SELECT p.id, p.oem_part_number, p.name, p.name_es,
                   pg.name AS group_name, pc.name AS category_name
            FROM parts_fts fts
            JOIN parts p ON fts.rowid = p.id
            JOIN part_groups pg ON p.group_id = pg.id
            JOIN part_categories pc ON pg.category_id = pc.id
            WHERE parts_fts MATCH ?
            ORDER BY rank
            LIMIT ? OFFSET ?
        """, (query, per_page, offset))

    def search_part_number(self, number):
        like = f'%{number}%'
        results = []
        # OEM
        oem = self._query("""
            SELECT p.id, p.oem_part_number AS part_number, p.name, p.name_es, 'OEM' AS type,
                   (SELECT b.name FROM brands b JOIN models m ON m.brand_id = b.id
                    JOIN model_year_engine mye ON mye.model_id = m.id
                    JOIN vehicle_parts vp ON vp.model_year_engine_id = mye.id
                    WHERE vp.part_id = p.id LIMIT 1) AS source
            FROM parts p WHERE p.oem_part_number LIKE ?
        """, (like,))
        results.extend(oem)
        # Aftermarket
        after = self._query("""
            SELECT ap.id, ap.part_number, ap.name, ap.name_es, 'AFTERMARKET' AS type,
                   m.name AS source
            FROM aftermarket_parts ap
            JOIN manufacturers m ON ap.manufacturer_id = m.id
            WHERE ap.part_number LIKE ?
        """, (like,))
        results.extend(after)
        # Cross-references
        xref = self._query("""
            SELECT pcr.id, pcr.cross_reference_number AS part_number,
                   p.name, p.name_es,
                   'CROSS-REF (' || pcr.reference_type || ')' AS type,
                   pcr.source
            FROM part_cross_references pcr
            JOIN parts p ON pcr.part_id = p.id
            WHERE pcr.cross_reference_number LIKE ?
        """, (like,))
        results.extend(xref)
        return results

    # === VIN ===

    def get_vin_cache(self, vin):
        return self._query("""
            SELECT * FROM vin_cache
            WHERE vin = ? AND expires_at > datetime('now')
        """, (vin,), one=True)

    def save_vin_cache(self, vin, data, make, model, year, engine_info, body_class, drive_type):
        self._execute("""
            INSERT OR REPLACE INTO vin_cache
            (vin, decoded_data, make, model, year, engine_info, body_class, drive_type, expires_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now', '+30 days'))
        """, (vin, data, make, model, year, engine_info, body_class, drive_type))

    # === Stats ===

    def get_stats(self):
        stats = {}
        tables = ['brands', 'models', 'years', 'engines', 'model_year_engine',
                   'part_categories', 'part_groups', 'parts', 'vehicle_parts',
                   'manufacturers', 'aftermarket_parts', 'part_cross_references',
                   'diagrams', 'diagram_hotspots', 'vin_cache']
        conn = self._connect()
        cursor = conn.cursor()
        for table in tables:
            cursor.execute(f"SELECT COUNT(*) FROM [{table}]")
            stats[table] = cursor.fetchone()[0]

        # Top brands with parts
        cursor.execute("""
            SELECT b.name, COUNT(vp.id) AS cnt
            FROM brands b
            JOIN models m ON m.brand_id = b.id
            JOIN model_year_engine mye ON mye.model_id = m.id
            JOIN vehicle_parts vp ON vp.model_year_engine_id = mye.id
            GROUP BY b.name ORDER BY cnt DESC LIMIT 5
        """)
        stats['top_brands'] = [dict(r) for r in cursor.fetchall()]
        conn.close()
        return stats

    # === Admin CRUD ===

    def create_part(self, data):
        return self._execute("""
            INSERT INTO parts (oem_part_number, name, name_es, group_id, description, description_es, weight_kg, material)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        """, (data['oem_part_number'], data['name'], data.get('name_es'), data['group_id'],
              data.get('description'), data.get('description_es'), data.get('weight_kg'), data.get('material')))

    def update_part(self, part_id, data):
        self._execute("""
            UPDATE parts SET oem_part_number=?, name=?, name_es=?, group_id=?,
                   description=?, description_es=?, weight_kg=?, material=?, is_discontinued=?
            WHERE id=?
        """, (data['oem_part_number'], data['name'], data.get('name_es'), data['group_id'],
              data.get('description'), data.get('description_es'), data.get('weight_kg'),
              data.get('material'), data.get('is_discontinued', 0), part_id))

    def delete_part(self, part_id):
        self._execute("DELETE FROM parts WHERE id=?", (part_id,))

    def create_manufacturer(self, data):
        return self._execute("""
            INSERT INTO manufacturers (name, type, quality_tier, country, website)
            VALUES (?, ?, ?, ?, ?)
        """, (data['name'], data.get('type', 'aftermarket'), data.get('quality_tier', 'standard'),
              data.get('country'), data.get('website')))

    def update_manufacturer(self, mfr_id, data):
        self._execute("""
            UPDATE manufacturers SET name=?, type=?, quality_tier=?, country=?, website=?
            WHERE id=?
        """, (data['name'], data.get('type'), data.get('quality_tier'),
              data.get('country'), data.get('website'), mfr_id))

    def delete_manufacturer(self, mfr_id):
        self._execute("DELETE FROM manufacturers WHERE id=?", (mfr_id,))

    def get_manufacturers(self):
        return self._query("SELECT * FROM manufacturers ORDER BY name")

    def create_crossref(self, data):
        return self._execute("""
            INSERT INTO part_cross_references (part_id, cross_reference_number, reference_type, source, notes)
            VALUES (?, ?, ?, ?, ?)
        """, (data['part_id'], data['cross_reference_number'], data.get('reference_type', 'interchange'),
              data.get('source'), data.get('notes')))

    def update_crossref(self, xref_id, data):
        self._execute("""
            UPDATE part_cross_references
            SET part_id=?, cross_reference_number=?, reference_type=?, source=?, notes=?
            WHERE id=?
        """, (data['part_id'], data['cross_reference_number'], data.get('reference_type'),
              data.get('source'), data.get('notes'), xref_id))

    def delete_crossref(self, xref_id):
        self._execute("DELETE FROM part_cross_references WHERE id=?", (xref_id,))

    def get_crossrefs_paginated(self, page=1, per_page=15):
        offset = (page - 1) * per_page
        return self._query("""
            SELECT pcr.*, p.oem_part_number, p.name AS part_name
            FROM part_cross_references pcr
            JOIN parts p ON pcr.part_id = p.id
            ORDER BY pcr.id
            LIMIT ? OFFSET ?
        """, (per_page, offset))

    def get_vehicles_for_part(self, part_id):
        return self._query("""
            SELECT b.name AS brand, m.name AS model, y.year, e.name AS engine
            FROM vehicle_parts vp
            JOIN model_year_engine mye ON vp.model_year_engine_id = mye.id
            JOIN models m ON mye.model_id = m.id
            JOIN brands b ON m.brand_id = b.id
            JOIN years y ON mye.year_id = y.id
            JOIN engines e ON mye.engine_id = e.id
            WHERE vp.part_id = ?
            ORDER BY b.name, m.name, y.year
        """, (part_id,))
```

**Step 4: Run tests**

```bash
cd /home/Autopartes && python -m pytest console/tests/test_db.py -v
```

Expected: All PASS

**Step 5: Commit**

```bash
git add console/db.py console/tests/
git commit -m "feat(console): add database abstraction layer with tests"
```

---

## Task 3: Core Framework — Key Bindings and Navigation

**Files:**
- Create: `console/core/keybindings.py`
- Create: `console/core/navigation.py`
- Create: `console/core/screens.py`
- Create: `console/tests/test_core.py`

**Step 1: Write tests**

```python
# console/tests/test_core.py
import os, sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))

from console.core.keybindings import Key, KeyBindings
from console.core.navigation import Navigation
from console.core.screens import Screen


def test_key_constants():
    assert Key.ESCAPE == 27
    assert Key.ENTER == 10
    assert Key.F1 is not None


def test_keybindings_register():
    kb = KeyBindings()
    called = []
    kb.bind(Key.F1, lambda: called.append('help'))
    kb.handle(Key.F1)
    assert called == ['help']


def test_keybindings_unbound_key():
    kb = KeyBindings()
    result = kb.handle(999)
    assert result is False


def test_navigation_push_pop():
    nav = Navigation()
    nav.push('menu', {})
    nav.push('brands', {'filter': 'T'})
    assert nav.current() == ('brands', {'filter': 'T'})
    nav.pop()
    assert nav.current() == ('menu', {})


def test_navigation_breadcrumb():
    nav = Navigation()
    nav.push('menu', {}, label='Menu')
    nav.push('brands', {}, label='Marcas')
    nav.push('models', {}, label='TOYOTA')
    bc = nav.breadcrumb()
    assert bc == ['Menu', 'Marcas', 'TOYOTA']


def test_navigation_empty():
    nav = Navigation()
    assert nav.current() is None
    nav.pop()  # should not crash


def test_screen_base():
    s = Screen('test', 'Test Screen')
    assert s.name == 'test'
    assert s.title == 'Test Screen'
```

**Step 2: Run test to verify failure**

```bash
cd /home/Autopartes && python -m pytest console/tests/test_core.py -v
```

**Step 3: Implement keybindings.py**

```python
# console/core/keybindings.py
import curses


class Key:
    """Key constants matching curses key codes."""
    ESCAPE = 27
    ENTER = 10
    TAB = 9
    BACKSPACE = 127
    UP = curses.KEY_UP
    DOWN = curses.KEY_DOWN
    LEFT = curses.KEY_LEFT
    RIGHT = curses.KEY_RIGHT
    PGUP = curses.KEY_PPAGE
    PGDN = curses.KEY_NPAGE
    HOME = curses.KEY_HOME
    END = curses.KEY_END
    F1 = curses.KEY_F1
    F2 = curses.KEY_F2
    F3 = curses.KEY_F3
    F4 = curses.KEY_F4
    F5 = curses.KEY_F5
    F6 = curses.KEY_F6
    F7 = curses.KEY_F7
    F8 = curses.KEY_F8
    F9 = curses.KEY_F9
    F10 = curses.KEY_F10


class KeyBindings:
    """Registry of key-to-action bindings."""

    def __init__(self):
        self._bindings = {}

    def bind(self, key, action):
        self._bindings[key] = action

    def handle(self, key):
        if key in self._bindings:
            self._bindings[key]()
            return True
        return False

    def get_footer_labels(self):
        """Return list of (key_label, description) for footer display."""
        return self._footer_labels if hasattr(self, '_footer_labels') else []

    def set_footer(self, labels):
        """Set footer labels: [('F1', 'Ayuda'), ('ESC', 'Atrás'), ...]"""
        self._footer_labels = labels
```

**Step 4: Implement navigation.py**

```python
# console/core/navigation.py


class Navigation:
    """Screen navigation stack with breadcrumb support."""

    def __init__(self):
        self._stack = []  # list of (screen_name, context, label)

    def push(self, screen_name, context=None, label=None):
        self._stack.append((screen_name, context or {}, label or screen_name))

    def pop(self):
        if self._stack:
            return self._stack.pop()
        return None

    def current(self):
        if self._stack:
            entry = self._stack[-1]
            return (entry[0], entry[1])
        return None

    def breadcrumb(self):
        return [entry[2] for entry in self._stack]

    def clear(self):
        self._stack.clear()

    def depth(self):
        return len(self._stack)
```

**Step 5: Implement screens.py**

```python
# console/core/screens.py


class Screen:
    """Base class for all application screens."""

    def __init__(self, name, title):
        self.name = name
        self.title = title

    def on_enter(self, context, db, renderer):
        """Called when screen becomes active. Override in subclasses."""
        pass

    def on_key(self, key, context, db, renderer, nav):
        """Handle key press. Return screen_name to navigate, None to stay, 'back' to pop."""
        return None

    def render(self, context, db, renderer):
        """Draw the screen. Override in subclasses."""
        pass
```

**Step 6: Run tests**

```bash
cd /home/Autopartes && python -m pytest console/tests/test_core.py -v
```

Expected: All PASS

**Step 7: Commit**

```bash
git add console/core/ console/tests/test_core.py
git commit -m "feat(console): add core framework - keybindings, navigation, screen base"
```

---

## Task 4: Utilities — Formatting and VIN API

**Files:**
- Create: `console/utils/formatting.py`
- Create: `console/utils/vin_api.py`
- Create: `console/tests/test_utils.py`

**Step 1: Write tests**

```python
# console/tests/test_utils.py
import os, sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))

from console.utils.formatting import format_currency, format_number, truncate, pad_right, format_table_row


def test_format_currency():
    assert format_currency(45.99) == '$45.99'
    assert format_currency(None) == '──'
    assert format_currency(0) == '$0.00'


def test_format_number():
    assert format_number(13685) == '13,685'
    assert format_number(0) == '0'


def test_truncate():
    assert truncate('Hello World', 8) == 'Hello...'
    assert truncate('Short', 10) == 'Short'


def test_pad_right():
    assert pad_right('Hi', 5) == 'Hi   '
    assert pad_right('Hello World', 5) == 'Hello'


def test_format_table_row():
    row = format_table_row(['ID', 'Name', 'Price'], [4, 20, 8])
    assert len(row) > 0
    assert 'ID' in row
```

**Step 2: Run test to verify failure**

```bash
cd /home/Autopartes && python -m pytest console/tests/test_utils.py -v
```

**Step 3: Implement formatting.py**

```python
# console/utils/formatting.py


def format_currency(value):
    if value is None:
        return '──'
    return f'${value:,.2f}'


def format_number(value):
    if value is None:
        return '0'
    return f'{int(value):,}'


def truncate(text, max_len):
    if text is None:
        return ''
    text = str(text)
    if len(text) <= max_len:
        return text
    return text[:max_len - 3] + '...'


def pad_right(text, width):
    if text is None:
        text = ''
    text = str(text)
    if len(text) > width:
        return text[:width]
    return text.ljust(width)


def format_table_row(values, widths, separator=' │ '):
    """Format a row with fixed column widths."""
    parts = []
    for val, width in zip(values, widths):
        parts.append(pad_right(str(val) if val is not None else '', width))
    return separator.join(parts)


def quality_bar(tier):
    """Return visual quality bar for quality tier."""
    bars = {
        'premium': '██████████',
        'oem': '███████████',
        'standard': '███████░░░',
        'economy': '█████░░░░░',
    }
    return bars.get(tier, '░░░░░░░░░░')
```

**Step 4: Implement vin_api.py**

```python
# console/utils/vin_api.py
import json
import requests
from console.config import NHTSA_API_URL


def decode_vin_nhtsa(vin):
    """Decode a VIN using NHTSA API. Returns dict with vehicle info."""
    try:
        response = requests.get(
            NHTSA_API_URL,
            params={'format': 'json'},
            timeout=10
        )
        # NHTSA uses path-based VIN
        response = requests.get(
            f"{NHTSA_API_URL}/{vin}",
            params={'format': 'json'},
            timeout=10
        )
        response.raise_for_status()
        data = response.json()

        results = data.get('Results', [])
        info = {}
        for item in results:
            var = item.get('Variable', '')
            val = item.get('Value')
            if val and val.strip():
                if var == 'Make':
                    info['make'] = val.strip()
                elif var == 'Model':
                    info['model'] = val.strip()
                elif var == 'Model Year':
                    info['year'] = int(val.strip())
                elif var == 'Body Class':
                    info['body_class'] = val.strip()
                elif var == 'Drive Type':
                    info['drive_type'] = val.strip()
                elif var == 'Displacement (L)':
                    info.setdefault('engine_info', {})['displacement_l'] = val.strip()
                elif var == 'Engine Number of Cylinders':
                    info.setdefault('engine_info', {})['cylinders'] = val.strip()
                elif var == 'Fuel Type - Primary':
                    info.setdefault('engine_info', {})['fuel_type'] = val.strip()
                elif var == 'Engine Brake (hp) From':
                    info.setdefault('engine_info', {})['power_hp'] = val.strip()

        engine = info.get('engine_info', {})
        if engine:
            parts = []
            if engine.get('displacement_l'):
                parts.append(f"{engine['displacement_l']}L")
            if engine.get('cylinders'):
                parts.append(f"{engine['cylinders']} cil.")
            if engine.get('fuel_type'):
                parts.append(engine['fuel_type'])
            engine['raw'] = ' '.join(parts)

        return info
    except Exception as e:
        return {'error': str(e)}
```

**Step 5: Run tests**

```bash
cd /home/Autopartes && python -m pytest console/tests/test_utils.py -v
```

Expected: All PASS

**Step 6: Commit**

```bash
git add console/utils/ console/tests/test_utils.py
git commit -m "feat(console): add formatting utils and VIN API client"
```

---

## Task 5: Curses Renderer (VT220 Mode)

**Files:**
- Create: `console/renderers/curses_renderer.py`
- Create: `console/renderers/base.py`

**Step 1: Write base renderer interface**

```python
# console/renderers/base.py


class BaseRenderer:
    """Abstract renderer interface. All renderers must implement these methods."""

    def init_screen(self):
        raise NotImplementedError

    def cleanup(self):
        raise NotImplementedError

    def get_size(self):
        """Return (height, width) of terminal."""
        raise NotImplementedError

    def clear(self):
        raise NotImplementedError

    def refresh(self):
        raise NotImplementedError

    def get_key(self):
        """Block and return key code."""
        raise NotImplementedError

    def draw_header(self, title, subtitle=''):
        raise NotImplementedError

    def draw_footer(self, key_labels):
        """key_labels: list of (key, description) tuples."""
        raise NotImplementedError

    def draw_menu(self, items, selected_index=0, title=''):
        """items: list of (number, label) tuples."""
        raise NotImplementedError

    def draw_table(self, headers, rows, widths, page_info=None, selected_row=-1):
        raise NotImplementedError

    def draw_detail(self, fields, title=''):
        """fields: list of (label, value) tuples."""
        raise NotImplementedError

    def draw_form(self, fields, focused_index=0, title=''):
        """fields: list of {label, value, width, type} dicts."""
        raise NotImplementedError

    def draw_filter_list(self, items, filter_text, selected_index, title=''):
        raise NotImplementedError

    def draw_comparison(self, columns, title=''):
        """columns: list of {header, rows} dicts for side-by-side."""
        raise NotImplementedError

    def draw_text(self, row, col, text, style='normal'):
        raise NotImplementedError

    def draw_box(self, top, left, height, width, title=''):
        raise NotImplementedError

    def show_message(self, text, msg_type='info'):
        """msg_type: 'info', 'error', 'confirm'. Returns True/False for confirm."""
        raise NotImplementedError

    def show_input(self, prompt, max_len=40):
        """Show input prompt and return typed string."""
        raise NotImplementedError
```

**Step 2: Implement curses_renderer.py**

This is the largest single file. It implements the full VT220 aesthetic with green-on-black, box drawing characters, and Pick-style screen layout.

```python
# console/renderers/curses_renderer.py
import curses
from console.renderers.base import BaseRenderer
from console.config import COLORS_VT220
from console.utils.formatting import pad_right, truncate


class CursesRenderer(BaseRenderer):

    def __init__(self):
        self.stdscr = None
        self.colors = {}

    def init_screen(self):
        self.stdscr = curses.initscr()
        curses.noecho()
        curses.cbreak()
        curses.curs_set(0)
        self.stdscr.keypad(True)

        if curses.has_colors():
            curses.start_color()
            curses.use_default_colors()
            self._init_colors()

    def _init_colors(self):
        color_map = {'black': 0, 'red': 1, 'green': 2, 'yellow': 3,
                     'blue': 4, 'magenta': 5, 'cyan': 6, 'white': 7}
        for i, (name, (fg, bg)) in enumerate(COLORS_VT220.items(), 1):
            if i < curses.COLOR_PAIRS:
                curses.init_pair(i, color_map.get(fg, 7), color_map.get(bg, 0))
                self.colors[name] = curses.color_pair(i)
        # Ensure defaults
        self.colors.setdefault('normal', curses.color_pair(0))

    def _style(self, name):
        return self.colors.get(name, self.colors.get('normal', 0))

    def cleanup(self):
        if self.stdscr:
            curses.nocbreak()
            self.stdscr.keypad(False)
            curses.echo()
            curses.endwin()

    def get_size(self):
        h, w = self.stdscr.getmaxyx()
        return h, w

    def clear(self):
        self.stdscr.clear()

    def refresh(self):
        self.stdscr.refresh()

    def get_key(self):
        return self.stdscr.getch()

    def draw_header(self, title, subtitle=''):
        h, w = self.get_size()
        header_text = f'  {title}'
        if subtitle:
            space = w - len(title) - len(subtitle) - 4
            header_text = f'  {title}{" " * max(space, 2)}{subtitle}  '
        header_text = header_text[:w].ljust(w)
        try:
            self.stdscr.addstr(0, 0, header_text, self._style('header') | curses.A_BOLD)
        except curses.error:
            pass
        # Separator line
        try:
            self.stdscr.addstr(1, 0, '─' * w, self._style('border'))
        except curses.error:
            pass

    def draw_footer(self, key_labels):
        h, w = self.get_size()
        # Separator
        try:
            self.stdscr.addstr(h - 2, 0, '─' * w, self._style('border'))
        except curses.error:
            pass
        # Key labels
        footer = '  ' + '  '.join(f'{k}={v}' for k, v in key_labels) + '  '
        footer = footer[:w].ljust(w)
        try:
            self.stdscr.addstr(h - 1, 0, footer[:w-1], self._style('footer'))
        except curses.error:
            pass

    def draw_menu(self, items, selected_index=0, title=''):
        h, w = self.get_size()
        start_row = 3
        if title:
            try:
                self.stdscr.addstr(start_row, 2, title, self._style('title') | curses.A_BOLD)
            except curses.error:
                pass
            start_row += 2

        for i, (num, label) in enumerate(items):
            row = start_row + i
            if row >= h - 3:
                break
            style = self._style('highlight') if i == selected_index else self._style('normal')
            prefix = '>' if i == selected_index else ' '
            text = f'  {prefix} {num}. {label}'
            try:
                self.stdscr.addstr(row, 0, pad_right(text, w), style)
            except curses.error:
                pass

    def draw_table(self, headers, rows, widths, page_info=None, selected_row=-1):
        h, w = self.get_size()
        start_row = 3

        # Header row
        header_line = '  #  │ '
        for hdr, width in zip(headers, widths):
            header_line += pad_right(hdr, width) + ' │ '
        try:
            self.stdscr.addstr(start_row, 0, header_line[:w], self._style('title') | curses.A_BOLD)
            self.stdscr.addstr(start_row + 1, 0, '─' * w, self._style('border'))
        except curses.error:
            pass

        # Data rows
        for i, row in enumerate(rows):
            y = start_row + 2 + i
            if y >= h - 4:
                break
            style = self._style('highlight') if i == selected_row else self._style('normal')
            line = f'  {i + 1:<3}│ '
            for val, width in zip(row, widths):
                line += pad_right(truncate(str(val) if val is not None else '', width), width) + ' │ '
            try:
                self.stdscr.addstr(y, 0, line[:w], style)
            except curses.error:
                pass

        # Page info
        if page_info:
            info_row = h - 3
            info_text = f'  Página {page_info.get("page", 1)}/{page_info.get("total_pages", 1)}' \
                        f'    {page_info.get("showing", "")} de {page_info.get("total", "")} registros'
            try:
                self.stdscr.addstr(info_row, 0, info_text, self._style('info'))
            except curses.error:
                pass

    def draw_detail(self, fields, title=''):
        h, w = self.get_size()
        start_row = 3
        if title:
            try:
                self.stdscr.addstr(start_row, 2, title, self._style('title') | curses.A_BOLD)
            except curses.error:
                pass
            start_row += 2

        max_label = max(len(f[0]) for f in fields) if fields else 10
        for i, (label, value) in enumerate(fields):
            row = start_row + i
            if row >= h - 3:
                break
            dots = '.' * (max_label - len(label) + 2)
            line = f'  {label}{dots}: {value}'
            try:
                self.stdscr.addstr(row, 2, label, self._style('field_label'))
                self.stdscr.addstr(row, 2 + len(label), dots + ': ', self._style('border'))
                self.stdscr.addstr(row, 2 + max_label + 4, str(value)[:w - max_label - 8],
                                   self._style('field_value'))
            except curses.error:
                pass

    def draw_form(self, fields, focused_index=0, title=''):
        h, w = self.get_size()
        start_row = 3
        if title:
            try:
                self.stdscr.addstr(start_row, 2, title, self._style('title') | curses.A_BOLD)
            except curses.error:
                pass
            start_row += 2

        max_label = max(len(f['label']) for f in fields) if fields else 10
        for i, field in enumerate(fields):
            row = start_row + i
            if row >= h - 3:
                break
            label = field['label']
            value = field.get('value', '')
            field_width = field.get('width', 30)
            dots = '.' * (max_label - len(label) + 2)
            is_focused = (i == focused_index)

            try:
                self.stdscr.addstr(row, 2, f'{i + 1}.', self._style('info'))
                self.stdscr.addstr(row, 5, label, self._style('field_label'))
                self.stdscr.addstr(row, 5 + len(label), dots + ': ', self._style('border'))

                val_col = 5 + max_label + 4
                display_val = f'[{pad_right(str(value), field_width)}]'
                style = self._style('field_active') if is_focused else self._style('field_value')
                self.stdscr.addstr(row, val_col, display_val[:w - val_col - 2], style)

                hint = field.get('hint', '')
                if hint:
                    hint_col = val_col + field_width + 3
                    if hint_col < w - 2:
                        self.stdscr.addstr(row, hint_col, hint[:w - hint_col - 1], self._style('info'))
            except curses.error:
                pass

    def draw_filter_list(self, items, filter_text, selected_index, title=''):
        h, w = self.get_size()
        start_row = 3
        if title:
            try:
                self.stdscr.addstr(start_row, 2, title, self._style('title') | curses.A_BOLD)
            except curses.error:
                pass
            start_row += 1
            self.stdscr.addstr(start_row, 0, '─' * w, self._style('border'))
            start_row += 1

        # Filter input
        try:
            self.stdscr.addstr(start_row, 2, 'Filtro: ', self._style('field_label'))
            self.stdscr.addstr(start_row, 10, filter_text + '_', self._style('field_active'))
        except curses.error:
            pass
        start_row += 1
        try:
            self.stdscr.addstr(start_row, 0, '─' * w, self._style('border'))
        except curses.error:
            pass
        start_row += 1

        # Items
        visible_items = h - start_row - 4
        scroll_offset = max(0, selected_index - visible_items + 1)
        for i, item in enumerate(items[scroll_offset:scroll_offset + visible_items]):
            row = start_row + i
            actual_idx = scroll_offset + i
            is_selected = actual_idx == selected_index
            prefix = '> ' if is_selected else '  '
            num = f'{actual_idx + 1}.'
            style = self._style('highlight') if is_selected else self._style('normal')
            text = f'  {prefix}{num:>4} {item}'
            try:
                self.stdscr.addstr(row, 0, pad_right(text, w), style)
            except curses.error:
                pass

        # Count
        count_text = f'  {len(items)} coincidencias'
        try:
            self.stdscr.addstr(h - 3, w - len(count_text) - 2, count_text, self._style('info'))
        except curses.error:
            pass

    def draw_comparison(self, columns, title=''):
        h, w = self.get_size()
        start_row = 3
        if title:
            try:
                self.stdscr.addstr(start_row, 2, title, self._style('title') | curses.A_BOLD)
            except curses.error:
                pass
            start_row += 2

        n_cols = len(columns)
        if n_cols == 0:
            return
        label_w = 16
        col_w = max(12, (w - label_w - 4) // n_cols - 3)

        # Column headers
        header_line = pad_right('', label_w) + ' │ '
        for col in columns:
            header_line += pad_right(truncate(col['header'], col_w), col_w) + ' │ '
        try:
            self.stdscr.addstr(start_row, 2, header_line[:w - 4], self._style('title') | curses.A_BOLD)
            self.stdscr.addstr(start_row + 1, 2, '─' * (w - 4), self._style('border'))
        except curses.error:
            pass
        start_row += 2

        # Find max rows
        max_rows = max(len(col.get('rows', [])) for col in columns) if columns else 0
        for r in range(max_rows):
            row = start_row + r
            if row >= h - 4:
                break
            # Row label from first column
            row_label = columns[0]['rows'][r][0] if r < len(columns[0].get('rows', [])) else ''
            line = pad_right(row_label, label_w) + ' │ '
            for col in columns:
                val = col['rows'][r][1] if r < len(col.get('rows', [])) else ''
                line += pad_right(truncate(str(val), col_w), col_w) + ' │ '
            try:
                self.stdscr.addstr(row, 2, line[:w - 4], self._style('normal'))
            except curses.error:
                pass

    def draw_text(self, row, col, text, style='normal'):
        h, w = self.get_size()
        if row < h and col < w:
            try:
                self.stdscr.addstr(row, col, text[:w - col], self._style(style))
            except curses.error:
                pass

    def draw_box(self, top, left, height, width, title=''):
        try:
            # Top border
            self.stdscr.addstr(top, left, '┌' + '─' * (width - 2) + '┐', self._style('border'))
            if title:
                self.stdscr.addstr(top, left + 2, f' {title} ', self._style('title') | curses.A_BOLD)
            # Sides
            for i in range(1, height - 1):
                self.stdscr.addstr(top + i, left, '│', self._style('border'))
                self.stdscr.addstr(top + i, left + width - 1, '│', self._style('border'))
            # Bottom border
            self.stdscr.addstr(top + height - 1, left, '└' + '─' * (width - 2) + '┘', self._style('border'))
        except curses.error:
            pass

    def show_message(self, text, msg_type='info'):
        h, w = self.get_size()
        box_w = min(len(text) + 6, w - 4)
        box_h = 5
        top = h // 2 - 2
        left = (w - box_w) // 2

        style = self._style('error') if msg_type == 'error' else self._style('info')
        self.draw_box(top, left, box_h, box_w)

        try:
            self.stdscr.addstr(top + 2, left + 3, truncate(text, box_w - 6), style)
            if msg_type == 'confirm':
                self.stdscr.addstr(top + 3, left + 3, '(S/N)', self._style('field_label'))
            else:
                self.stdscr.addstr(top + 3, left + 3, 'Presione cualquier tecla...', self._style('field_label'))
        except curses.error:
            pass
        self.refresh()

        if msg_type == 'confirm':
            while True:
                key = self.get_key()
                if key in (ord('s'), ord('S'), ord('y'), ord('Y')):
                    return True
                if key in (ord('n'), ord('N'), 27):
                    return False
        else:
            self.get_key()
            return True

    def show_input(self, prompt, max_len=40):
        h, w = self.get_size()
        box_w = max(len(prompt) + max_len + 8, 40)
        box_h = 5
        top = h // 2 - 2
        left = (w - box_w) // 2

        self.draw_box(top, left, box_h, box_w, prompt)

        value = ''
        curses.curs_set(1)
        while True:
            try:
                display = f'[{pad_right(value, max_len)}]'
                self.stdscr.addstr(top + 2, left + 3, display, self._style('field_active'))
                self.stdscr.move(top + 2, left + 4 + len(value))
            except curses.error:
                pass
            self.refresh()

            key = self.get_key()
            if key == 10:  # ENTER
                break
            elif key == 27:  # ESC
                curses.curs_set(0)
                return None
            elif key in (127, curses.KEY_BACKSPACE, 8):
                value = value[:-1]
            elif 32 <= key <= 126 and len(value) < max_len:
                value += chr(key)

        curses.curs_set(0)
        return value
```

**Step 3: Commit**

```bash
git add console/renderers/
git commit -m "feat(console): add curses VT220 renderer with full widget set"
```

---

## Task 6: App Controller and Main Menu Screen

**Files:**
- Create: `console/core/app.py`
- Create: `console/screens/menu_principal.py`
- Create: `console/screens/estadisticas.py`

**Step 1: Implement app.py**

```python
# console/core/app.py
from console.core.navigation import Navigation
from console.core.keybindings import Key


class App:
    """Main application controller. Manages screen lifecycle and navigation."""

    def __init__(self, renderer, db):
        self.renderer = renderer
        self.db = db
        self.nav = Navigation()
        self.screens = {}
        self.running = False
        self._register_screens()

    def _register_screens(self):
        from console.screens.menu_principal import MenuPrincipal
        from console.screens.estadisticas import EstadisticasScreen
        from console.screens.vehiculo_nav import VehiculoNavScreen
        from console.screens.buscar_parte import BuscarParteScreen
        from console.screens.buscar_texto import BuscarTextoScreen
        from console.screens.vin_decoder import VinDecoderScreen
        from console.screens.catalogo import CatalogoScreen
        from console.screens.parte_detalle import ParteDetalleScreen
        from console.screens.comparador import ComparadorScreen
        from console.screens.admin_partes import AdminPartesScreen
        from console.screens.admin_fabricantes import AdminFabricantesScreen
        from console.screens.admin_crossref import AdminCrossrefScreen
        from console.screens.admin_import import AdminImportScreen

        self.screens = {
            'menu': MenuPrincipal(),
            'estadisticas': EstadisticasScreen(),
            'vehiculo_nav': VehiculoNavScreen(),
            'buscar_parte': BuscarParteScreen(),
            'buscar_texto': BuscarTextoScreen(),
            'vin_decoder': VinDecoderScreen(),
            'catalogo': CatalogoScreen(),
            'parte_detalle': ParteDetalleScreen(),
            'comparador': ComparadorScreen(),
            'admin_partes': AdminPartesScreen(),
            'admin_fabricantes': AdminFabricantesScreen(),
            'admin_crossref': AdminCrossrefScreen(),
            'admin_import': AdminImportScreen(),
        }

    def run(self):
        self.renderer.init_screen()
        self.running = True
        self.nav.push('menu', {}, label='Menú')

        try:
            while self.running:
                current = self.nav.current()
                if current is None:
                    break

                screen_name, context = current
                screen = self.screens.get(screen_name)
                if screen is None:
                    self.renderer.show_message(f'Pantalla "{screen_name}" no encontrada', 'error')
                    self.nav.pop()
                    continue

                # Render
                self.renderer.clear()
                screen.render(context, self.db, self.renderer)
                self.renderer.refresh()

                # Get input
                key = self.renderer.get_key()

                # Global keys
                if key == Key.F10:
                    self.nav.clear()
                    self.nav.push('menu', {}, label='Menú')
                    continue

                # Screen-specific handling
                result = screen.on_key(key, context, self.db, self.renderer, self.nav)

                if result == 'quit':
                    self.running = False
                elif result == 'back':
                    self.nav.pop()
                elif result and isinstance(result, tuple):
                    # (screen_name, context, label)
                    name, ctx, label = result
                    self.nav.push(name, ctx, label=label)
                elif result and isinstance(result, str):
                    self.nav.push(result, {}, label=result)

        except KeyboardInterrupt:
            pass
        finally:
            self.renderer.cleanup()
```

**Step 2: Implement menu_principal.py**

```python
# console/screens/menu_principal.py
from console.core.screens import Screen
from console.core.keybindings import Key
from console.config import APP_NAME, APP_SUBTITLE, VERSION


class MenuPrincipal(Screen):

    def __init__(self):
        super().__init__('menu', 'Menú Principal')
        self.selected = 0
        self.items = [
            ('1', 'Consulta por Vehículo'),
            ('2', 'Búsqueda por Número de Parte'),
            ('3', 'Búsqueda por Descripción'),
            ('4', 'Decodificador VIN'),
            ('5', 'Catálogo de Categorías'),
            ('─', '─────────────────────────'),
            ('6', 'Administración de Partes'),
            ('7', 'Administración de Fabricantes'),
            ('8', 'Cross-References'),
            ('9', 'Importar / Exportar Datos'),
            ('─', '─────────────────────────'),
            ('0', 'Estadísticas del Sistema'),
        ]
        self.selectable = [i for i, (num, _) in enumerate(self.items) if num != '─']
        self.actions = {
            '1': ('vehiculo_nav', {}, 'Vehículo'),
            '2': ('buscar_parte', {}, 'Buscar Parte'),
            '3': ('buscar_texto', {}, 'Buscar Texto'),
            '4': ('vin_decoder', {}, 'VIN Decoder'),
            '5': ('catalogo', {}, 'Catálogo'),
            '6': ('admin_partes', {}, 'Admin Partes'),
            '7': ('admin_fabricantes', {}, 'Admin Fabricantes'),
            '8': ('admin_crossref', {}, 'Admin Cross-Ref'),
            '9': ('admin_import', {}, 'Importar/Exportar'),
            '0': ('estadisticas', {}, 'Estadísticas'),
        }

    def render(self, context, db, renderer):
        renderer.draw_header(f'{APP_NAME} v{VERSION}', APP_SUBTITLE)
        renderer.draw_menu(self.items, self.selected)
        renderer.draw_footer([
            ('F1', 'Ayuda'), ('F3', 'Buscar'), ('F10', 'Menú'), ('ESC', 'Salir')
        ])

    def on_key(self, key, context, db, renderer, nav):
        if key == Key.ESCAPE:
            if renderer.show_message('¿Desea salir del sistema?', 'confirm'):
                return 'quit'
            return None

        # Number keys
        if 48 <= key <= 57:  # 0-9
            num = chr(key)
            if num in self.actions:
                return self.actions[num]

        # Arrow navigation
        if key == Key.DOWN:
            idx = self.selectable.index(self.selected) if self.selected in self.selectable else 0
            idx = min(idx + 1, len(self.selectable) - 1)
            self.selected = self.selectable[idx]
        elif key == Key.UP:
            idx = self.selectable.index(self.selected) if self.selected in self.selectable else 0
            idx = max(idx - 1, 0)
            self.selected = self.selectable[idx]
        elif key == Key.ENTER:
            num = self.items[self.selected][0]
            if num in self.actions:
                return self.actions[num]

        return None
```

**Step 3: Implement estadisticas.py**

```python
# console/screens/estadisticas.py
from console.core.screens import Screen
from console.core.keybindings import Key
from console.utils.formatting import format_number


class EstadisticasScreen(Screen):

    def __init__(self):
        super().__init__('estadisticas', 'Estadísticas del Sistema')

    def render(self, context, db, renderer):
        import datetime
        now = datetime.datetime.now().strftime('%d/%b/%Y %H:%M')
        renderer.draw_header('ESTADÍSTICAS DEL SISTEMA', now)

        stats = db.get_stats()

        fields = [
            ('', '── BASE DE DATOS ─────────────────────────────────'),
            ('Marcas', format_number(stats.get('brands', 0))),
            ('Modelos', format_number(stats.get('models', 0))),
            ('Motores', format_number(stats.get('engines', 0))),
            ('Configuraciones', format_number(stats.get('model_year_engine', 0))),
            ('Fabricantes', format_number(stats.get('manufacturers', 0))),
            ('Categorías', format_number(stats.get('part_categories', 0))),
            ('Grupos', format_number(stats.get('part_groups', 0))),
            ('Partes OEM', format_number(stats.get('parts', 0))),
            ('Aftermarket', format_number(stats.get('aftermarket_parts', 0))),
            ('Cross-Refs', format_number(stats.get('part_cross_references', 0))),
            ('Diagramas', format_number(stats.get('diagrams', 0))),
            ('', '── COBERTURA ─────────────────────────────────────'),
            ('Fitments', format_number(stats.get('vehicle_parts', 0))),
            ('VINs en caché', format_number(stats.get('vin_cache', 0))),
        ]

        # Add top brands
        top_brands = stats.get('top_brands', [])
        if top_brands:
            brands_str = '  '.join(f"{b['name']}({format_number(b['cnt'])})" for b in top_brands)
            fields.append(('Top marcas', brands_str))

        renderer.draw_detail(fields, 'ESTADÍSTICAS DEL SISTEMA')
        renderer.draw_footer([('F5', 'Actualizar'), ('ESC', 'Menú')])

    def on_key(self, key, context, db, renderer, nav):
        if key == Key.ESCAPE:
            return 'back'
        if key == Key.F5:
            return None  # Re-render
        return None
```

**Step 4: Commit**

```bash
git add console/core/app.py console/screens/menu_principal.py console/screens/estadisticas.py
git commit -m "feat(console): add app controller, main menu and statistics screen"
```

---

## Task 7: Vehicle Navigation Screen (Drill-Down)

**Files:**
- Create: `console/screens/vehiculo_nav.py`

**Step 1: Implement drill-down navigation**

The vehicle navigation screen handles the full drill-down flow: Brand → Model → Year → Engine → Categories → Groups → Parts, each as a filterable list. It uses the renderer's `draw_filter_list` method and maintains state for each drill-down level within its context dict.

**Key behaviors:**
- Typing characters filters the list in real time
- Arrow keys move selection cursor
- ENTER advances to next drill-down level
- ESC goes back one level (or exits to menu if at brand level)
- After selecting engine, transitions to catalogo screen with the vehicle's mye_id

**Step 2: Commit**

```bash
git add console/screens/vehiculo_nav.py
git commit -m "feat(console): add vehicle drill-down navigation screen"
```

---

## Task 8: Catalog, Search, and VIN Screens

**Files:**
- Create: `console/screens/catalogo.py`
- Create: `console/screens/buscar_parte.py`
- Create: `console/screens/buscar_texto.py`
- Create: `console/screens/vin_decoder.py`

**Step 1: Implement catalogo.py**

Categories → Groups → Parts drill-down, similar structure to vehiculo_nav but for the parts hierarchy. Uses `draw_filter_list` for categories/groups and `draw_table` for parts list.

**Step 2: Implement buscar_parte.py**

Single input field (via `show_input`), calls `db.search_part_number()`, displays results in `draw_table`. Selecting a result navigates to `parte_detalle`.

**Step 3: Implement buscar_texto.py**

Same pattern as buscar_parte but uses `db.search_parts()` (FTS5) with a text query. Paginated results via PgUp/PgDn.

**Step 4: Implement vin_decoder.py**

Input 17-char VIN, check `db.get_vin_cache()` first, else call `decode_vin_nhtsa()` and cache result. Display decoded info via `draw_detail`. Option to navigate to vehicle parts.

**Step 5: Commit**

```bash
git add console/screens/catalogo.py console/screens/buscar_parte.py console/screens/buscar_texto.py console/screens/vin_decoder.py
git commit -m "feat(console): add catalog, search, and VIN decoder screens"
```

---

## Task 9: Part Detail and Comparator Screens

**Files:**
- Create: `console/screens/parte_detalle.py`
- Create: `console/screens/comparador.py`

**Step 1: Implement parte_detalle.py**

Top section: `draw_detail` with part fields (OEM number, name, group, category, description, material, weight). Bottom section: `draw_table` with aftermarket alternatives. F4 navigates to cross-references view. F6 shows vehicles that use this part. Number key selects an alternative to compare.

**Step 2: Implement comparador.py**

Uses `draw_comparison` to show OEM part and selected aftermarket alternatives side by side. Shows quality bars, prices, savings percentages, warranty info.

**Step 3: Commit**

```bash
git add console/screens/parte_detalle.py console/screens/comparador.py
git commit -m "feat(console): add part detail and comparator screens"
```

---

## Task 10: Admin CRUD Screens

**Files:**
- Create: `console/screens/admin_partes.py`
- Create: `console/screens/admin_fabricantes.py`
- Create: `console/screens/admin_crossref.py`
- Create: `console/screens/admin_import.py`

**Step 1: Implement admin_partes.py**

List view with `draw_table` (paginated). F2 or ENTER to edit selected part via `draw_form`. F3 to add new part. F9 saves, ESC cancels. Delete with confirmation dialog.

**Step 2: Implement admin_fabricantes.py**

Same CRUD pattern as admin_partes but for manufacturers table.

**Step 3: Implement admin_crossref.py**

Same CRUD pattern for cross-references. F1 on part_id field opens lookup list of parts.

**Step 4: Implement admin_import.py**

Menu to select data type (categories, groups, parts, manufacturers, aftermarket, crossref, fitment). File path input via `show_input`. Reads CSV and calls appropriate db methods. Shows progress/results.

**Step 5: Commit**

```bash
git add console/screens/admin_*.py
git commit -m "feat(console): add admin CRUD screens for parts, manufacturers, crossref, import"
```

---

## Task 11: Textual/Rich Modern Renderer

**Files:**
- Create: `console/renderers/textual_renderer.py`

**Step 1: Install textual dependency**

```bash
pip install textual rich
```

**Step 2: Implement textual_renderer.py**

Implements the same `BaseRenderer` interface but using textual/rich for a modern TUI experience. Uses Rich tables, panels, and styled text instead of curses box drawing. The same screen logic drives both renderers.

Note: textual uses an async event loop, so this renderer wraps textual's App in a synchronous interface matching BaseRenderer. An alternative is a thin adapter that translates `draw_*` calls into Rich console output for simpler integration.

**Step 3: Commit**

```bash
git add console/renderers/textual_renderer.py
git commit -m "feat(console): add textual/rich modern renderer"
```

---

## Task 12: Integration Testing and Polish

**Files:**
- Create: `console/tests/test_integration.py`
- Modify: `console/main.py`

**Step 1: Write integration tests**

Test that the app can be instantiated, screens registered, and basic navigation works (using a mock renderer that records draw calls instead of painting to screen).

**Step 2: Add error handling to main.py**

Wrap `app.run()` in try/except to always clean up the terminal on crash. Add `--db` flag to override database path.

**Step 3: Test full app manually**

```bash
cd /home/Autopartes && python -m console.main --mode vt220
cd /home/Autopartes && python -m console.main --mode modern
```

**Step 4: Commit**

```bash
git add console/
git commit -m "feat(console): add integration tests and polish error handling"
```

---

## Task 13: Documentation and Final Commit

**Files:**
- Create: `console/README.md`
- Modify: `README.md` (root)

**Step 1: Write console README**

Usage instructions, keybindings reference, screenshots placeholder, mode descriptions.

**Step 2: Update root README**

Add section about the console system with quickstart command.

**Step 3: Final commit**

```bash
git add console/README.md README.md
git commit -m "docs: add console system documentation"
```

---

## Summary

| Task | Description | Est. Steps |
|------|-------------|------------|
| 1 | Project scaffold + config | 4 |
| 2 | Database abstraction layer | 5 |
| 3 | Core framework (keys, nav, screens) | 7 |
| 4 | Utilities (formatting, VIN API) | 6 |
| 5 | Curses VT220 renderer | 3 |
| 6 | App controller + main menu + stats | 4 |
| 7 | Vehicle drill-down navigation | 2 |
| 8 | Catalog, search, VIN screens | 5 |
| 9 | Part detail + comparator | 3 |
| 10 | Admin CRUD screens | 5 |
| 11 | Textual modern renderer | 3 |
| 12 | Integration tests + polish | 4 |
| 13 | Documentation | 3 |
| **Total** | | **54 steps** |

**Dependencies:** Tasks 1-5 must be completed first (foundation). Tasks 6-10 can be done in any order after foundation. Task 11 is independent of 6-10. Tasks 12-13 are final.
