"""Fill in an asset's vendor from the catalog model it already points at.

An asset that carries a model but no vendor is showing a blank the database can
answer: the model records its own vendor, and both sides reference the same
vendors table. The detail pages fall back to it at display time, but the record
itself stays empty, so the edit form shows an empty vendor box and any report
reading vendorid directly still sees nothing. This writes it down.

ONLY fills rows where the asset's vendor is NULL and the model names one. It
never overwrites a vendor somebody chose, and it never guesses: if the model has
no vendor either, the row is left alone.

It also fills the asset's TYPE from the model's type, but ONLY where the two
names are identical. modeltypes is the catalog-wide list covering every kind of
asset - it holds "Access Point", "Camera" and "Desktop PC" alongside the machine
entries - so it is a different taxonomy from machinetypes and the two cannot be
equated in general. Across the whole catalog only about two thirds of the names
overlap. Restricted to the models an asset class actually uses the picture is
different: every one of the 262 machines in the development database maps
exactly, because the non-machine entries are never used by machines.

So the rule is exact name match or nothing. A model type with no same-named
entry in the asset's own type table is REPORTED and left alone, never guessed
at, because the failure mode is a machine labelled "Desktop PC".

Dry run by default; nothing is written without --commit.

    python scripts/backfill_vendor_from_model.py
    python scripts/backfill_vendor_from_model.py --commit
    python scripts/backfill_vendor_from_model.py --only machines --commit
"""

import argparse
import os
import sys

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

# Each asset table that carries both a model link and its own vendor, with the
# column that identifies a row in reports and logs.
TARGETS = [
    ('machines',       'machineid',       'machinename'),
    ('computers',      'computerid',      'computername'),
    ('printers',       'printerid',       'printername'),
    ('networkdevices', 'networkdeviceid', 'hostname'),
]

# The asset's own type column, and the table it points at. Each asset class
# keeps its own type list; the catalog's modeltypes is a different taxonomy, so
# these are joined by NAME and only when the name matches exactly.
TYPE_TARGETS = {
    'machines':       ('machinetypeid',       'machinetypes',       'machinetype'),
    'computers':      ('computertypeid',      'computertypes',      'computertype'),
    'printers':       ('printertypeid',       'printertypes',       'printertype'),
    'networkdevices': ('networkdevicetypeid', 'networkdevicetypes', 'networkdevicetype'),
}


def resolve(connection, table, idcol, labelcol):
    """Rows that would change, newest table columns tolerated."""
    from sqlalchemy import text
    columns = {c['name'] for c in __import__('sqlalchemy').inspect(connection).get_columns(table)}
    if 'modelnumberid' not in columns or 'vendorid' not in columns:
        return None, []
    label = labelcol if labelcol in columns else idcol
    rows = connection.execute(text(f"""
        SELECT a.{idcol}          AS assetid,
               a.{label}          AS label,
               m.modelnumber      AS modelnumber,
               m.vendorid         AS vendorid,
               v.vendor           AS vendorname
        FROM {table} a
        JOIN models m  ON a.modelnumberid = m.modelnumberid
        JOIN vendors v ON m.vendorid = v.vendorid
        WHERE a.vendorid IS NULL
          AND m.vendorid IS NOT NULL
        ORDER BY a.{idcol}
    """)).fetchall()
    return label, rows


def resolve_types(connection, table, idcol, labelcol):
    """Rows whose type could be taken from the model, plus the ones that cannot.

    Returns (fillable, unmatched). unmatched rows have a model type that has no
    identically named entry in this asset's own type table, and are never
    touched - they are reported so somebody can decide.
    """
    from sqlalchemy import inspect, text
    spec = TYPE_TARGETS.get(table)
    if not spec:
        return [], []
    typecol, typetable, typenamecol = spec
    columns = {c['name'] for c in inspect(connection).get_columns(table)}
    if 'modelnumberid' not in columns or typecol not in columns:
        return [], []
    label = labelcol if labelcol in columns else idcol

    fillable = connection.execute(text(f"""
        SELECT a.{idcol} AS assetid, a.{label} AS label,
               mt.modeltype AS modeltype, tt.{typenamecol} AS typename
        FROM {table} a
        JOIN models mo     ON a.modelnumberid = mo.modelnumberid
        JOIN modeltypes mt ON mo.modeltypeid = mt.modeltypeid
        JOIN {typetable} tt ON tt.{typenamecol} = mt.modeltype
        WHERE a.{typecol} IS NULL
        ORDER BY a.{idcol}
    """)).fetchall()

    unmatched = connection.execute(text(f"""
        SELECT mt.modeltype AS modeltype, COUNT(*) AS n
        FROM {table} a
        JOIN models mo     ON a.modelnumberid = mo.modelnumberid
        JOIN modeltypes mt ON mo.modeltypeid = mt.modeltypeid
        LEFT JOIN {typetable} tt ON tt.{typenamecol} = mt.modeltype
        WHERE a.{typecol} IS NULL AND tt.{typenamecol} IS NULL
        GROUP BY mt.modeltype ORDER BY n DESC
    """)).fetchall()
    return fillable, unmatched


def main():
    parser = argparse.ArgumentParser(description=__doc__,
                                     formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument('--commit', action='store_true',
                        help='write the changes (default is a dry run)')
    parser.add_argument('--only', metavar='TABLE',
                        help='restrict to one table, e.g. machines')
    parser.add_argument('--limit', type=int, default=20,
                        help='rows to list per table in the preview (default 20)')
    args = parser.parse_args()

    # FLASK_ENV is deliberately NOT forced. The app reads it from .env, which on
    # a server already says production; overriding it here demanded a SECRET_KEY
    # the environment had no reason to provide and the script could not run.
    from sqlalchemy import text
    from shopdb import create_app
    from shopdb.extensions import db

    app = create_app()
    with app.app_context():
        connection = db.session.connection()
        total = 0
        for table, idcol, labelcol in TARGETS:
            if args.only and args.only != table:
                continue
            label, rows = resolve(connection, table, idcol, labelcol)
            if label is None:
                print(f'{table}: no model or vendor column here, skipped')
                continue
            print(f'\n{table}: {len(rows)} row(s) would get a vendor')
            for row in rows[:args.limit]:
                print(f'    {row.assetid:>7}  {str(row.label)[:28]:<28} '
                      f'model {row.modelnumber[:22]:<22} -> {row.vendorname}')
            if len(rows) > args.limit:
                print(f'    ... and {len(rows) - args.limit} more')
            total += len(rows)

            if args.commit and rows:
                connection.execute(text(f"""
                    UPDATE {table} a
                    JOIN models m ON a.modelnumberid = m.modelnumberid
                    SET a.vendorid = m.vendorid
                    WHERE a.vendorid IS NULL
                      AND m.vendorid IS NOT NULL
                """))

            # Type, by exact name only.
            typed, unmatched = resolve_types(connection, table, idcol, labelcol)
            spec = TYPE_TARGETS.get(table)
            print(f'{table}: {len(typed)} row(s) would get a type')
            for row in typed[:args.limit]:
                print(f'    {row.assetid:>7}  {str(row.label)[:28]:<28} '
                      f'model type {row.modeltype[:22]:<22} -> {row.typename}')
            if len(typed) > args.limit:
                print(f'    ... and {len(typed) - args.limit} more')
            for row in unmatched:
                print(f'    LEFT ALONE: {row.n} row(s) with model type '
                      f'"{row.modeltype}" - no matching {spec[1]} entry')
            total += len(typed)

            if args.commit and typed:
                typecol, typetable, typenamecol = spec
                connection.execute(text(f"""
                    UPDATE {table} a
                    JOIN models mo      ON a.modelnumberid = mo.modelnumberid
                    JOIN modeltypes mt  ON mo.modeltypeid = mt.modeltypeid
                    JOIN {typetable} tt ON tt.{typenamecol} = mt.modeltype
                    SET a.{typecol} = tt.{typecol}
                    WHERE a.{typecol} IS NULL
                """))

        if args.commit:
            db.session.commit()
            print(f'\nCommitted. {total} row(s) updated.')
        else:
            print(f'\nDRY RUN - nothing written. {total} row(s) would change.')
            print('Re-run with --commit to apply.')
    return 0


if __name__ == '__main__':
    sys.exit(main())
