import time, json, os
from concurrent.futures import ThreadPoolExecutor, as_completed

from dotenv import load_dotenv
load_dotenv()

from flask import Flask, request, Response, session, send_file
from flask_session import Session

from urllib.parse import urlencode, urlparse
import requests

import socket
from io import BytesIO

import fitz  # PyMuPDF
from PIL import Image

from reportlab.lib.pagesizes import inch
from reportlab.pdfgen import canvas

from reportlab.lib import colors
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase import pdfmetrics


import io
import threading
import pymysql
import logging

# --- Zebra printer functions --------------------------------------------------

def print_pdf_to_zebra(
    pdf_path: str,
    env_var: str = "ZEBRA_PRINTER_IP",
    port: int = 9100,
    dpi: int = 203,
    threshold: int = 180,
    timeout: float = 10.0,
) -> None:
    """
    Render a multi-page PDF at 203 dpi, convert each page to monochrome,
    encode as ZPL ^GFA, and send to a Zebra printer over TCP:9100.

    Args:
        pdf_path: Path to the PDF file.
        env_var: Environment variable containing the printer IP.
        port: Zebra raw TCP port, usually 9100.
        dpi: Target printer resolution. GK420t is 203 dpi.
        threshold: Grayscale threshold for black/white conversion.
        timeout: Socket timeout in seconds.

    Raises:
        RuntimeError: If the printer IP environment variable is missing.
        FileNotFoundError: If the PDF file does not exist.
        ValueError: If the PDF is empty.
    """
    printer_ip = os.environ.get(env_var)
    if not printer_ip:
        raise RuntimeError(
            f"Missing printer IP. Set the environment variable {env_var}."
        )

    if not os.path.isfile(pdf_path):
        raise FileNotFoundError(pdf_path)

    doc = fitz.open(pdf_path)
    if len(doc) == 0:
        raise ValueError("PDF has no pages.")

    try:
        with socket.create_connection((printer_ip, port), timeout=timeout) as sock:
            for page_index in range(len(doc)):
                page = doc.load_page(page_index)

                # Render the PDF page directly at 203 dpi.
                pix = page.get_pixmap(dpi=dpi, alpha=False)

                # Convert pixmap to PIL image.
                img = Image.open(BytesIO(pix.tobytes("png")))

                # Convert to grayscale, then to pure black/white.
                gray = img.convert("L")
                bw = gray.point(lambda p: 0 if p < threshold else 255, mode="1")

                zpl = _image_to_zpl_page(bw)
                sock.sendall(zpl)

    finally:
        doc.close()


def _image_to_zpl_page(img: Image.Image) -> bytes:
    """
    Convert a 1-bit PIL image into a full ZPL label using ^GFA.
    One PDF page becomes one printed label.
    """
    if img.mode != "1":
        img = img.convert("1")

    width, height = img.size
    bytes_per_row = (width + 7) // 8
    total_bytes = bytes_per_row * height

    pixels = img.load()
    hex_rows = []

    for y in range(height):
        row = bytearray()
        for byte_x in range(bytes_per_row):
            value = 0
            for bit in range(8):
                x = byte_x * 8 + bit
                value <<= 1
                if x < width:
                    # In PIL mode "1": 0 = black, 255 = white
                    if pixels[x, y] == 0:
                        value |= 1
            row.append(value)
        hex_rows.append(row.hex().upper())

    hex_data = "".join(hex_rows)

    # ^PW = print width in dots
    # ^LL = label length in dots
    # ^FO0,0 = place image at top-left
    # ^GFA = inline ASCII hex graphic
    zpl = (
        "^XA\n"
        "^CI28\n"
        f"^PW{width}\n"
        f"^LL{height}\n"
        "^LH0,0\n"
        "^FO0,0\n"
        f"^GFA,{total_bytes},{total_bytes},{bytes_per_row},{hex_data}\n"
        "^PQ1\n"
        "^XZ\n"
    )

    return zpl.encode("ascii")

# --- MySQL cache helpers --------------------------------------------------

def get_db_connection():
    # read connection parameters from environment variables
    return pymysql.connect(
        host=os.environ.get('MYSQL_HOST', 'localhost'),
        user=os.environ.get('MYSQL_USER', 'root'),
        password=os.environ.get('MYSQL_PASSWORD', ''),
        db=os.environ.get('MYSQL_DB', 'fortnox'),
        charset='utf8mb4',
        cursorclass=pymysql.cursors.DictCursor,
        autocommit=True,
    )


def init_db():
    # create cache table if it doesn't already exist
    conn = get_db_connection()
    with conn.cursor() as cursor:
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS article_cache (
                ArticleNumber VARCHAR(255) PRIMARY KEY,
                StockAccount INT,
                StockChangeAccount INT,
                QuantityInStock DOUBLE,
                PurchasePrice DOUBLE,
                comptype VARCHAR(255),
                value VARCHAR(255),
                `case` VARCHAR(255),
                rating VARCHAR(255),
                `precision` VARCHAR(255),
                drift VARCHAR(255),
                extra_amt DOUBLE,
                ECCN VARCHAR(255),
                HTSUS VARCHAR(255),
                COO VARCHAR(255),
                place VARCHAR(255),
                place_old VARCHAR(255),
                last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
            )
        """)
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS missing_articles (
                ArticleNumber VARCHAR(255),
                ProductionOrder INT,
                quantity DOUBLE,
                quantity_required DOUBLE DEFAULT 0,
                PRIMARY KEY (ArticleNumber, ProductionOrder)
            )
        """)
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS orderplanning (
                id INT AUTO_INCREMENT PRIMARY KEY,
                ArticleNumber VARCHAR(255) NOT NULL,
                amount INT DEFAULT 0,
                price DOUBLE DEFAULT 0,
                supplier_id VARCHAR(255),
                supplier_currency VARCHAR(10),
                supplier_order_number VARCHAR(255),
                comment TEXT,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                UNIQUE KEY unique_article (ArticleNumber)
            )
        """)
        cursor.execute("""
            ALTER TABLE orderplanning MODIFY COLUMN supplier_id VARCHAR(255)
        """)
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS productionorders (
                production_order_number INT,
                article VARCHAR(255),
                quantity DOUBLE,
                PRIMARY KEY (production_order_number, article)
            )
        """)
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS procured_articles (
                purchase_order_number INT,
                article VARCHAR(255),
                quantity DOUBLE,
                PRIMARY KEY (purchase_order_number, article)
            )
        """)
    conn.close()


# ensure table exists on startup
init_db()


def get_cached_article(article_number):
    conn = get_db_connection()
    try:
        with conn.cursor() as cursor:
            cursor.execute("SELECT * FROM article_cache WHERE ArticleNumber=%s", (article_number,))
            return cursor.fetchone()
    finally:
        conn.close()


def update_cache(article_number, fields):
    if not fields:
        return
    # Ensure numeric columns are stored as NULL instead of empty string
    numeric_fields = {'StockAccount', 'StockChangeAccount', 'QuantityInStock', 'PurchasePrice', 'extra_amt'}
    for n in numeric_fields:
        if n in fields and (fields[n] == '' or fields[n] is None):
            fields[n] = None
        elif n in fields:
            try:
                # keep ints as-is, convert numeric strings
                if isinstance(fields[n], str) and fields[n].strip() != '':
                    fields[n] = float(fields[n])
            except Exception:
                fields[n] = None

    try:
        conn = get_db_connection()
        try:
            with conn.cursor() as cursor:
                keys = list(fields.keys())
                cols = ",".join([f"`{k}`" for k in keys])
                placeholders = ",".join(["%s"] * len(keys))
                updates = ",".join([f"`{k}`=VALUES(`{k}`)" for k in keys])
                sql = f"INSERT INTO article_cache (ArticleNumber,{cols}) VALUES (%s,{placeholders}) ON DUPLICATE KEY UPDATE {updates}"
                cursor.execute(sql, [article_number] + [fields[k] for k in keys])
        finally:
            conn.close()
    except Exception as e:
        logging.error(f"Failed to update cache for {article_number}: {e}")


# https://clerk.com/blog/oauth2-react-user-authorization
# root@netcup:/var/www/fortnox-react-flask-app# yarn start-api



app = Flask(__name__)

app.secret_key = os.environ.get("APP_SECRET_KEY")
app.config["SESSION_PERMANENT"] = False
app.config["SESSION_TYPE"] = "filesystem"
app.config["SESSION_FILE_DIR"] = os.environ.get(
    "SESSION_FILE_DIR", "/var/lib/fortnox/flask_session"
)
app.config["SESSION_PERMANENT"] = False
# Create and initialize the Flask-Session object AFTER `app` has been configured
server_session = Session(app)

CLIENT_ID       = os.environ.get("FORTNOX_CLIENT_ID")
CLIENT_SECRET   = os.environ.get("FORTNOX_CLIENT_SECRET")
REDIRECT_URI    = os.environ.get("FORTNOX_REDIRECT_URI")
FORTNOX_API_URI = os.environ.get("FORTNOX_API_URI")


def get_access_token(grant_type):
    logging.info(f"get_access_token called with grant_type={grant_type}")
    
    try:
        if grant_type == 'authorization_code':
            logging.info("Preparing authorization_code grant")
            if 'code' not in session:
                logging.error("No 'code' in session")
                return Response('{"message": "No authorization code in session", "loggedIn": false}', status=400, mimetype='application/json')
            
            params = {
                'grant_type'    : 'authorization_code',
                'code'          : session['code'],
                'redirect_uri'  : REDIRECT_URI,
            }
            logging.info(f"Code: {session['code'][:20]}... (truncated)")
            logging.info(f"Redirect URI: {REDIRECT_URI}")
        elif grant_type == 'refresh_token':
            logging.info("Preparing refresh_token grant")
            if 'refresh_token' not in session:
                logging.error("No 'refresh_token' in session")
                return Response('{"message": "No refresh token in session", "loggedIn": false}', status=400, mimetype='application/json')
            
            params = {
                'grant_type'    : 'refresh_token',
                'refresh_token' : session['refresh_token'],
            }
            logging.info(f"Refresh token: {session['refresh_token'][:20]}... (truncated)")
        else:
            logging.error(f"Unknown grant_type: {grant_type}")
            return False
        
        token_endpoint = 'https://apps.fortnox.se/oauth-v1/token'
        logging.info(f"POST to {token_endpoint}")
        
        if not CLIENT_ID or not CLIENT_SECRET:
            logging.error("CLIENT_ID or CLIENT_SECRET not set in environment")
            return Response('{"message": "Missing CLIENT_ID or CLIENT_SECRET", "loggedIn": false}', status=500, mimetype='application/json')
        
        response = requests.post(
            url=token_endpoint, 
            data=params, 
            auth=(CLIENT_ID, CLIENT_SECRET),
            timeout=30
        )
        
        logging.info(f"Token response status: {response.status_code}")
        
        try:
            response_data = response.json()
        except:
            logging.error(f"Failed to parse response JSON: {response.text}")
            return Response('{"message": "Invalid JSON response from token endpoint", "loggedIn": false}', status=500, mimetype='application/json')
        
        if response.status_code != 200:
            logging.error(f"Token endpoint returned {response.status_code}: {response_data}")
            return Response(json.dumps({"message": f"Token endpoint error: {response_data}", "loggedIn": False}), status=500, mimetype='application/json')
        
        if 'error' in response_data:
            logging.error(f"Token error: {response_data.get('error')} - {response_data.get('error_description')}")
            return Response(json.dumps({"message": f"Fortnox auth error: {response_data.get('error')}", "loggedIn": False}), status=500, mimetype='application/json')
        
        if 'access_token' not in response_data:
            logging.error(f"No access_token in response: {response_data}")
            return Response('{"message": "No access_token in response", "loggedIn": false}', status=500, mimetype='application/json')
        
        logging.info("Successfully obtained access token")
        session['access_token']     = response_data['access_token']
        session['expires']          = time.time() + response_data.get('expires_in', 3600) - 5
        session['refresh_token']    = response_data.get('refresh_token')
        session['scope']            = response_data.get('scope', '')
        session['token_type']       = response_data.get('token_type', 'Bearer')
        
        logging.info(f"Session updated. Token expires at: {session['expires']}")
        
        try:
            load_accounts()
            logging.info("Accounts loaded successfully")
        except Exception as e:
            logging.warning(f"Failed to load accounts: {e}")
        
        return True
        
    except requests.exceptions.HTTPError as errh:
        logging.error(f"HTTP Error: {errh.args[0]}")
        return Response(json.dumps({"message": f"HTTP Error: {str(errh)}", "loggedIn": False}), status=500, mimetype='application/json')
    except requests.exceptions.ReadTimeout as errrt:
        logging.error(f"Request timeout: {errrt}")
        return Response('{"message": "Request timeout", "loggedIn": false}', status=500, mimetype='application/json')
    except requests.exceptions.ConnectionError as conerr:
        logging.error(f"Connection error: {conerr}")
        return Response('{"message": "Connection error", "loggedIn": false}', status=500, mimetype='application/json')
    except requests.exceptions.RequestException as errex:
        logging.error(f"Request exception: {errex}")
        return Response('{"message": "Request exception", "loggedIn": false}', status=500, mimetype='application/json')
    except KeyError as error:
        logging.error(f"KeyError in token response: {error}")
        try:
            logging.error(f"Response data: {response_data}")
        except:
            pass
        return Response('{"message": "Missing required field in token response", "loggedIn": false}', status=500, mimetype='application/json')
    except Exception as e:
        logging.error(f"Unexpected error in get_access_token: {e}", exc_info=True)
        return Response(json.dumps({"message": f"Unexpected error: {str(e)}", "loggedIn": False}), status=500, mimetype='application/json')


@app.route('/auth/logged_in')
def auth_logged_in():
    logging.info("auth_logged_in called")
    if 'access_token' in session:
        logging.info(f"access_token found in session, expires at {session['expires']}, current time {time.time()}")
        if session['expires'] < time.time():
            logging.info("Token expired, attempting refresh")
            token_response = get_access_token('refresh_token')
            if isinstance(token_response, Response):
                logging.error("Token refresh failed with Response")
                session.pop('access_token', None)
                session.pop('expires', None)
                session.pop('refresh_token', None)
                session.pop('scope', None)
                session.pop('token_type', None)
                return token_response
            if not token_response:
                logging.error("Token refresh returned False")
                session.pop('access_token', None)
                session.pop('expires', None)
                session.pop('refresh_token', None)
                session.pop('scope', None)
                session.pop('token_type', None)
                return {'loggedIn': False}
            else:
                logging.info("Token refresh successful")
                return {'loggedIn': True, 'name': 'holger refreshed token', 'expires': session['expires'], 'time': time.time(), 'response': str(type(token_response))}
        logging.info("Token still valid")
        return {'loggedIn': True, 'name': 'holger', 'session': str(session['expires']) + ' ' + str(time.time())}
    else:
        logging.info("No access_token in session")
        return {'loggedIn': False}

@app.route('/auth/token')   
def auth_token():
    logging.info("auth_token called")
    code = request.args.get("code")
    logging.info(f"Received code: {code[:20] if code else 'None'}... (truncated)")
    
    if not code or code == '':
        logging.error("No authorization code provided")
        return Response('{"message": "Authorization code must be provided"}', status=400, mimetype='application/json')

    session['code'] = code
    logging.info("Code stored in session, calling get_access_token")
    
    token_result = get_access_token('authorization_code')
    logging.info(f"get_access_token returned: {type(token_result)}")
    
    if isinstance(token_result, Response):
        logging.error("get_access_token returned Response (error)")
        return token_result
    
    if token_result is True:
        logging.info("Login successful")
        return {
            "email": "user@example.com",
            "name": "John Doe",
            "picture": "http://example.com/picture.jpg"
        }
    else:
        logging.error(f"get_access_token returned: {token_result}")
        return Response('{"message": "Failed to obtain access token"}', status=500, mimetype='application/json')


@app.route('/auth/logout', methods=['GET', 'POST'])
def auth_logout():
    logging.info("auth_logout called")
    session.clear()
    logging.info("Session cleared")
    return {'message': 'Logged out'}

@app.route('/auth/url')
def auth_url():
    logging.info("auth_url called")
    logging.info(f"CLIENT_ID: {CLIENT_ID[:20] if CLIENT_ID else 'NOT SET'}...")
    logging.info(f"REDIRECT_URI: {REDIRECT_URI}")
    
    if not CLIENT_ID:
        logging.error("CLIENT_ID not set in environment")
        return {'error': 'CLIENT_ID not configured'}
    
    authorization_endpoint = 'https://apps.fortnox.se/oauth-v1/auth'
    params = {
        'client_id'     : CLIENT_ID,
        'redirect_uri'  : REDIRECT_URI,
        'scope'         : 'warehouse article bookkeeping supplier order',
        'state'         : 'somestate123',
        'access_type'   : 'offline',
        'response_type' : 'code',
    }
    
    auth_url = authorization_endpoint + '?' + urlencode(params)
    logging.info(f"Generated auth URL (first 100 chars): {auth_url[:100]}...")
    return {'url': auth_url}

@app.route('/suppliers')
def api_suppliers():
    """
    Returns list of all suppliers from Fortnox API with their IDs, names, and currency.
    """
    headers = {"Authorization": "Bearer "+session['access_token']}
    suppliers = []
    offset = 0
    limit = 100

    try:
        while True:
            response = requests.get(
                url=FORTNOX_API_URI+f"3/suppliers?limit={limit}&offset={offset}",
                headers=headers,
                timeout=30
            )
            response.raise_for_status()
            response_data = response.json()

            if not response_data or 'Suppliers' not in response_data:
                break

            page = response_data['Suppliers']
            for supplier in page:
                num = supplier.get('SupplierNumber', '')
                suppliers.append({
                    'id': str(num) if num else '',
                    'name': supplier.get('SupplierName', supplier.get('Name', '')),
                    'supplierNumber': str(num) if num else '',
                    'currency': supplier.get('Currency', '')
                })

            if len(page) < limit:
                break
            offset += limit

        return suppliers
    except Exception as e:
        logging.error(f"Failed to fetch suppliers: {e}")
        try:
            logging.error(f"Response text: {response.text}")
        except:
            pass
        return Response(json.dumps({'error': str(e)}), status=500, mimetype='application/json')

@app.route('/warehouse')
def warehouse():
    headers = {"Authorization": "Bearer "+session['access_token']}
    articles = []
    offset = 0
    row_id = 0
    while True:
        url = FORTNOX_API_URI+f"3/articles?limit=500&offset={offset}"
        response = requests.get(url = url, headers=headers)
        response_data = response.json()
        # If API returned an error or an unexpected structure, return it directly
        if not response_data or 'Articles' not in response_data:
            # If we already collected articles, return what we have, otherwise return API response
            if len(articles) > 0:
                break
            return response_data
        page = response_data['Articles']
        for row in page:
            # normalize id field
            row["id"] = row.pop("ArticleNumber")

            row_id = row_id + 1
            row["documentid"] = ""
            row["supplier"] = ""
            row["orderdate"] = ""
            row["po_id"] = ""
            row["po_qty"] = ""
            row["row_id"] = row_id
            row["po_name"] = ""



            # Fetch and merge cached data
            cached_data = get_cached_article(row["id"])
            if cached_data:
                for key, value in cached_data.items():
                    if value is not None and key != 'ArticleNumber':
                        row[key] = value
                        row[f"cached_{key}"] = True
            
            articles.append(row)
        # If fewer than page size returned, we're done
        if len(page) < 500:
            break
        offset += 500

    return articles

@app.route('/warehouse/productionorders')
def warehouse_productionorders():
    headers = {"Authorization": "Bearer "+session['access_token']}
    response = requests.get(url = FORTNOX_API_URI+"api/warehouse/productionorders-v1?state=all", headers=headers)

    response_data = response.json()
    
    # Update productionorders table
    conn = get_db_connection()
    try:
        with conn.cursor() as cursor:
            # Get list of orders to keep (not voided, not completed)
            orders_to_keep = []
            if isinstance(response_data, dict) and 'ProductionOrders' in response_data:
                orders_list = response_data['ProductionOrders']
            elif isinstance(response_data, list):
                orders_list = response_data
            else:
                orders_list = []
            
            # Clear the table first
            cursor.execute("DELETE FROM productionorders")
            
            # Insert ongoing orders (not voided, not completed)
            for order in orders_list:
                if order.get('documentState') not in ('completed', 'voided'):
                    order_id = order.get('id') or order.get('Id')
                    article = order.get('itemId') or order.get('ItemId') or ''
                    quantity = order.get('quantity') or order.get('Quantity') or 0
                    cursor.execute(
                        "INSERT INTO productionorders (production_order_number, article, quantity) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE quantity=%s",
                        (int(order_id), article, quantity, quantity)
                    )
            
            conn.commit()
    finally:
        conn.close()
    
    return response_data


@app.route('/warehouse/purchaseorders')
def warehouse_purchaseorders():
    headers = {"Authorization": "Bearer "+session['access_token']}
    response = requests.get(url = FORTNOX_API_URI+"api/warehouse/purchaseorders-v1?state=all", headers=headers)

    response_data = response.json()

    if isinstance(response_data, dict) and 'PurchaseOrders' in response_data:
        orders_list = response_data['PurchaseOrders']
    elif isinstance(response_data, list):
        orders_list = response_data
    else:
        orders_list = []

    active_orders = [o for o in orders_list if o.get('purchaseOrderState') not in ('VOIDED', 'RECEIVED')]

    def fetch_order_details(order_id):
        try:
            r = requests.get(
                url=FORTNOX_API_URI + f"api/warehouse/purchaseorders-v1/{int(order_id)}",
                headers=headers,
                timeout=20
            )
            return order_id, r.json()
        except Exception as e:
            logging.error(f"Failed to fetch purchase order {order_id}: {e}")
            return order_id, None

    order_details = {}
    with ThreadPoolExecutor(max_workers=3) as executor:
        future_to_id = {executor.submit(fetch_order_details, o.get('id')): o.get('id') for o in active_orders}
        for future in as_completed(future_to_id):
            order_id, details = future.result()
            if details:
                order_details[str(order_id)] = details

    conn = get_db_connection()
    try:
        with conn.cursor() as cursor:
            cursor.execute("DELETE FROM procured_articles")
            for order_id, details in order_details.items():
                for row in details.get('rows', []):
                    article = row.get('itemId') or ''
                    quantity = row.get('orderedQuantity') or 0
                    if article:
                        cursor.execute(
                            "INSERT INTO procured_articles (purchase_order_number, article, quantity) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE quantity=%s",
                            (int(order_id), article, quantity, quantity)
                        )
    finally:
        conn.close()

    return response_data
#             warehouse/productionorder/

@app.route('/warehouse/productionorder/<id>')
def warehouse_productionorder(id = 0):
    headers = {"Authorization": "Bearer "+session['access_token']}
    response = requests.get(url = FORTNOX_API_URI+"api/warehouse/productionorders-v1/"+str(int(id)), headers=headers)
    row_id = 0
    response_data = response.json()
    for row in response_data['packageItems']:
        row_id = row_id + 1
        row["id"] = row.pop("itemId")
        row["documentid"] = str(int(id))
        row["supplier"] = ""
        row["orderdate"] = ""
        row["po_id"] = int(id)
        row["po_qty"] = response_data["quantity"]
        row["row_id"] = row_id
        row["po_name"] = response_data["itemId"]
        row['Description'] = row.pop('itemDescription')

        # attach cached values if available and mark them
        cached = get_cached_article(row["id"])
        if cached:
            for key in [
                'StockAccount','QuantityInStock','PurchasePrice',
                'comptype','value','case','rating','precision',
                'drift','extra_amt','ECCN','HTSUS','COO','place','place_old'
            ]:
                if key in cached and cached[key] is not None:
                    row[key] = cached[key]
                    row[f"cached_{key}"] = True
        # schedule asynchronous refresh of cache
        refresh_cache_async(row["id"])

    # Calculate quantity missing and store in missing_articles
    conn = get_db_connection()
    with conn.cursor() as cursor:
        for row in response_data['packageItems']:
            quantity_missing = row.get('totalQuantityRequired', 0) - row.get('quantityReserved', 0)
            row['quantityMissing'] = quantity_missing
            total_required = row.get('totalQuantityRequired', 0)
            cursor.execute("INSERT INTO missing_articles (ArticleNumber, ProductionOrder, quantity, quantity_required) VALUES (%s, %s, %s, %s) ON DUPLICATE KEY UPDATE quantity=%s, quantity_required=%s", (row['id'], int(id), quantity_missing, total_required, quantity_missing, total_required))
    conn.close()

    return response_data


@app.route('/warehouse/purchaseorder/<id>')
def warehouse_purchaseorder(id = 0):
    headers = {"Authorization": "Bearer "+session['access_token']}

    response = requests.get(url = FORTNOX_API_URI+"api/warehouse/purchaseorders-v1/"+str(int(id)), headers=headers)
  #  return response.json()
    response_data = response.json()
    row_id = 0
    for row in response_data['rows']:
        row_id = row_id + 1
        row["id"] = row.pop("itemId")
        row["documentid"] = "PO"+str(response_data["id"])
        row["supplier"] = response_data["supplierName"]
        row["orderdate"] = response_data["orderDate"]
        row["po_id"] = int(id)
        row["po_qty"] = row.get('orderedQuantity', 0)
        row["row_id"] = row_id
        row["po_name"] = response_data["supplierName"]
        row['Description'] = row.pop('itemDescription')
        # merge cached data for this article
        cached = get_cached_article(row["id"])
        if cached:
            for key in [
                'StockAccount','QuantityInStock','PurchasePrice',
                'comptype','value','case','rating','precision',
                'drift','extra_amt','ECCN','HTSUS','COO','place','place_old'
            ]:
                if key in cached and cached[key] is not None:
                    row[key] = cached[key]
                    row[f"cached_{key}"] = True
        refresh_cache_async(row["id"])
    response_data['packageItems'] = response_data['rows']

    return response_data


@app.route('/warehouse/procured-articles')
def warehouse_procured_articles():
    conn = get_db_connection()
    try:
        with conn.cursor() as cursor:
            cursor.execute("SELECT * FROM procured_articles ORDER BY purchase_order_number, article")
            rows = cursor.fetchall()
    finally:
        conn.close()
    return json.dumps(rows)


@app.route('/accounts')
def accounts():
    return session['accounts']
    headers = {"Authorization": "Bearer "+session['access_token']}
    response = requests.get(url = FORTNOX_API_URI+"3/accounts?limit=500", headers=headers)
    response_data = response.json()
    return response_data
    return_data = []
    for i in response_data['Accounts']:
#        response_data['Accounts'][i]['Number']
        account = {'value':i['Number'], 'label':str(i['Number'])+" "+i['Description']}
        #account = str(i['Number'])
        return_data.append(account)
    return return_data

def load_accounts():
    headers = {"Authorization": "Bearer "+session['access_token']}
    accounts = []
    offset = 0
    while True:
        response = requests.get(url = FORTNOX_API_URI+"3/accounts?limit=500&offset="+str(offset), headers=headers)
        response_data = response.json()
 #   return response_data
    #    accounts = []
        for i in response_data['Accounts']:
#        response_data['Accounts'][i]['Number']
            if not i['Active']:
                continue
            account = {'value':i['Number'], 'label':str(i['Number'])+" "+i['Description']}
        #account = str(i['Number'])
            accounts.append(account)
        if len(response_data['Accounts']) < 500:
            break
        else:
            offset += 500
    session['accounts'] = accounts  
    return accounts




def article_data(id=0, token=None):
    if token is None:
        token = session['access_token']
    id_masked = id.replace("SLASH","%2F")
    id_masked = id_masked.replace("+","%2B")
    id_masked = id_masked.replace("?","%3F")
    id_masked = id_masked.replace(":","%3A")
    id = id.replace("SLASH","/")
    headers = {"Authorization": "Bearer "+token}

    # Fortnox rate limits can return 429; retry a few times with exponential backoff.
    backoff = 1.0
    for attempt in range(3):
        response = requests.get(url = FORTNOX_API_URI+"3/articles/"+id_masked, headers=headers)
        if response.status_code == 429:
            logging.warning(f"Fortnox rate limit hit for {id} (429); sleeping {backoff}s")
            time.sleep(backoff)
            backoff *= 2
            continue
        break

    if response.status_code == 429:
        logging.error(f"Fortnox rate limit persistent for {id}; giving up")
        return {'ArticleNumber': id}

    try:
        response_data = response.json()
    except Exception as e:
        logging.error(f"Failed to parse Fortnox response for {id}: {e} (status {response.status_code}) body={response.text!r}")
        return {'ArticleNumber': id}

    # return response_data
    note = "{"+response_data['Article'].pop('Note')+"}"
    note = note.replace('\n','')
    note = json.loads(note)
    response_data['Article'].update(note)

    # update cache asynchronously with the fields we're interested in
    try:
        fields = {k: response_data['Article'].get(k) for k in [
            'StockAccount','StockChangeAccount','QuantityInStock','PurchasePrice',
            'comptype','value','case','rating','precision',
            'drift','extra_amt','ECCN','HTSUS','COO','place','place_old'
        ]}
        threading.Thread(target=update_cache, args=(response_data['Article']['ArticleNumber'], fields), daemon=True).start()
    except Exception as e:
        logging.error(f"Failed to initiate cache update for {response_data['Article']['ArticleNumber']}: {e}")

    return response_data['Article']

# helper used by order endpoints to refresh cached article details in the background
# rate‑limit processing to 4 calls per second using a queue & worker thread

_refresh_queue = []
_refresh_lock = threading.Lock()

def _refresh_worker():
    while True:
        item = None
        with _refresh_lock:
            if _refresh_queue:
                item = _refresh_queue.pop(0)
        if item is not None:
            article_number, token = item
            try:
                # call article_data which will update the cache
                article_data(article_number, token)
            except Exception as e:
                logging.error(f"refresh_cache failed for {article_number}: {e}")
        # enforce ~2 requests per second to reduce Fortnox rate limiting
        time.sleep(0.5)

# start worker thread once
threading.Thread(target=_refresh_worker, daemon=True).start()


def refresh_cache_async(article_number):
    with _refresh_lock:
        # avoid unbounded queue growth; can also dedupe if desired
        _refresh_queue.append((article_number, session['access_token']))


@app.route('/article/<id>')
def article(id = 0):
    # Always fetch fresh data
    article = article_data(id)
    if 'ArticleNumber' in article and len(article) == 1:
        return Response("{\"error\": \"Unable to fetch article data, possibly due to rate limit\"}", status=429, mimetype='application/json')
    # Since it's fresh, no cached flags needed
    article['referrer_url'] = urlparse(request.referrer)
    parts = article['referrer_url'].path.split('/')
    if len(parts) >= 3 and parts[-2] == 'productionorder':
        try:
            productionorder = int(parts[-1])
        except ValueError:
            productionorder = 0

        if productionorder != 0:
            if 'newQuantityInStock' in session and productionorder in session['newQuantityInStock'] and id in session['newQuantityInStock'][productionorder]:
                if session['newQuantityInStock'][productionorder][id]['new'] == article.get('QuantityInStock'):
                    article['newQuantityInStock'] = ''
                else:
                    article['newQuantityInStock'] = session['newQuantityInStock'][productionorder][id]['new']
            else:
                article['newQuantityInStock'] = ''
    return article


@app.route('/update/article/<id>', methods=['POST'])
def update_article(id = 0):
    article = article_data(id)
    headers = {"Authorization": "Bearer "+session['access_token'], 
               "Content-type" : "application/json"
    }
    id_masked = id.replace("SLASH","%2F")
    id_masked = id_masked.replace("+","%2B")
    id_masked = id_masked.replace("?","%3F")
    id_masked = id_masked.replace(":","%3A")
    id = id.replace("SLASH","/")
    #str(request.args.get("newRow"))
    data = json.loads(request.data)
    newRow = data['newRow']
    oldRow = data['oldRow']
#    return {'old':oldRow,'new':newRow}
    
    differences = {key: (newRow[key], oldRow[key]) for key in newRow if (key in oldRow) and (newRow[key] != oldRow[key])}
#    return differences
    differences2 = {key: (newRow[key], "") for key in newRow if not key in oldRow}
    differences.update(differences2)
    update = {}
    if len(differences) == 0:
        return Response("{\"message\": \"No field changed.\"}", status=500, mimetype='application/json')
    if len(differences) != 1:
        return Response("{\"message\": \"More than one field changed.\"}", status=500, mimetype='application/json')
    key = next(iter(differences))
    custom_fields = ["comptype","value","case","rating","precision","drift","extra_amt","ECCN","HTSUS","COO","place","place_old"]
    if key in custom_fields:
        newdata = {}
        for element in custom_fields:
            if element ==  key:
                newdata[element] = newRow[element]
            else:
                if element in article:
                    newdata[element] = article[element]
                else:
                    newdata[element] = ""
        note_txt = json.dumps(newdata)
        note_txt = note_txt.replace('{','')
        note_txt = note_txt.replace('}','')
        note_txt = note_txt.replace('", "','",\n"')
        update['Note'] = note_txt;
#        return newdata

#    return differences
  #  request.args.get("newRow")
    fields = ["Description","itemDescription","Manufacturer","StockAccount","StockChangeAccount"]
    if key in fields:
        if key == 'itemDescription': # production order list uses different key...
            update['Description'] = newRow[key]
        else:
            update[key] = newRow[key]

    params = {
        'Article' : update
    }
    if key == 'newQuantityInStock':
#        return json.dumps(session)
 #       return session['accounts']
 #       return session['newQuantityInStock']
  #      return 1 #str(type(session['newQuantityInStock']).__name__)
 #       return json.dumps(session['newQuantityInStock'])
        referrer_url = urlparse(request.referrer)
        url_parts = referrer_url.path.split('/')
        if len(url_parts) < 2:
            response = {
                'error'     : 'referrer error',
                'message'   : 'cannot parse referrer '+referrer_url,
                'params'    : params,
                'diff'      : differences,
            }
            return Response(json.dumps(response), status=500, mimetype='application/json')
        productionorder = int(url_parts[-1])
        if not productionorder != 0 or not url_parts[-2] == 'productionorder':
            response = {
                'error'     : 'wrong productionorder or type',
                'message'   : 'wrong productionorder id '+str(productionorder)+' or type '+url_parts[-2],
                'params'    : params,
                'diff'      : differences,
            }
            return Response(json.dumps(response), status=500, mimetype='application/json')
        if not 'newQuantityInStock' in session:
            session['newQuantityInStock'] = {}
#        session['newQuantityInStock'] = {}
        if not productionorder in session['newQuantityInStock']:
            session['newQuantityInStock'][productionorder] = {}
        if newRow[key] != oldRow[key]:
#            session['newQuantityInStock'] = newRow[key]
            session['newQuantityInStock'][productionorder][id] = { 'new': newRow[key], 'old' : oldRow['QuantityInStock']}
         #   session['newQuantityInStock'][productionorder]["test"] = newRow[key]
        else:
            session['newQuantityInStock'][productionorder].pop(id,None)
        session.modified = True
        return json.dumps(session['newQuantityInStock'])
        return session['newQuantityInStock']
#    return params
    response = requests.put(url = FORTNOX_API_URI+"3/articles/"+id_masked, data = json.dumps(params), headers=headers)
#    return response#.json()
    
    response_data = response.json()
    if "ErrorInformation" in response_data:
        response = {
            'error'     : response_data['ErrorInformation']['code'],
            'message'   : response_data['ErrorInformation']['message'],
            'params'    : params,
            'diff'      : differences,
        }
        return Response(json.dumps(response), status=500, mimetype='application/json')
    # Update cache with new value
    if key in custom_fields:
        update_cache(id, newdata)
    else:
        update_cache(id, {key: newRow[key]})
    return response_data

#@app.route('/update/articles/<id>', methods=['GET'])
def update_articles(id = 0):
  #  article = article_data(id)
    headers = {"Authorization": "Bearer "+session['access_token'], 
               "Content-type" : "application/json"
    }
    id = id.replace("SLASH","%2F")
    id = id.replace("+","%2B")
    id = id.replace("?","%3F")
    id = id.replace(":","%3A")
    id = id.replace("/","%2F")
    response = requests.get(url = FORTNOX_API_URI+"3/articles/"+id, headers=headers)
    response_data = response.json()
    if not 'Article' in response_data:
        response_data['error'] = 'error'
        return response_data
    if not 'Note' in response_data['Article']:
        response_data['error'] = 'nonote'
        return response_data
#    else:
#        return '1'
    note = "{"+response_data['Article'].pop('Note')+"}"
    note = note.replace('\n','')
    note = json.loads(note)
#    if not 'type' in note:
#        response_data['error'] = 'emptynote'
#        return response_data
    note['place'] = response_data['Article']['StockPlace']
    note['comptype'] = note.pop('comptype',note.pop('type',''))
    note_txt = json.dumps(note)
    note_txt = note_txt.replace('{','')
    note_txt = note_txt.replace('}','')
    note_txt = note_txt.replace('", "','",\n"')
    update = {}
    update['Article'] = {}
    update['Article']['Note'] = note_txt
    response = requests.put(url = FORTNOX_API_URI+"3/articles/"+id, data=json.dumps(update), headers=headers)
    return response.json()

    return update




@app.route('/warehouse/missingcomponents', methods=['GET'])
def warehouse_missingcomponents():
    """
    Returns aggregated missing components from the missing_articles table.
    Groups by ArticleNumber, sums quantities, and lists production order numbers.
    """
    headers = {"Authorization": "Bearer "+session['access_token']}
    
    # Clean up missing_articles for voided and completed production orders
    try:
        response_orders = requests.get(url=FORTNOX_API_URI+"api/warehouse/productionorders-v1?state=all", headers=headers)
        all_orders = response_orders.json()
        orders_list = all_orders.get('ProductionOrders', []) if isinstance(all_orders, dict) else all_orders
        
        # Get voided and completed order IDs
        completed_voided_ids = [str(o.get('id') or o.get('Id')) for o in orders_list if o.get('documentState') in ('Completed', 'Voided')]
        
        if completed_voided_ids:
            conn = get_db_connection()
            try:
                with conn.cursor() as cursor:
                    placeholders = ','.join(['%s'] * len(completed_voided_ids))
                    cursor.execute(f"DELETE FROM missing_articles WHERE ProductionOrder IN ({placeholders})", completed_voided_ids)
            finally:
                conn.close()
    except Exception as e:
        logging.error(f"Failed to clean up missing_articles for voided/completed orders: {e}")
    
    conn = get_db_connection()
    try:
        with conn.cursor() as cursor:
            cursor.execute("""
                SELECT
                    ma.ArticleNumber,
                    SUM(ma.quantity) as total_missing,
                    SUM(ma.quantity_required) as total_required,
                    GROUP_CONCAT(ma.ProductionOrder ORDER BY ma.ProductionOrder) as production_orders,
                    COALESCE(MAX(po_agg.total_qty), 0) as quantity_in_production,
                    COALESCE(MAX(pa_agg.total_qty), 0) as quantity_in_procurement
                FROM missing_articles ma
                LEFT JOIN (
                    SELECT article, SUM(quantity) as total_qty FROM productionorders GROUP BY article
                ) po_agg ON po_agg.article = ma.ArticleNumber
                LEFT JOIN (
                    SELECT article, SUM(quantity) as total_qty FROM procured_articles GROUP BY article
                ) pa_agg ON pa_agg.article = ma.ArticleNumber
                WHERE ma.quantity > 0
                GROUP BY ma.ArticleNumber
                ORDER BY total_missing DESC
            """)
            results = cursor.fetchall()
            
            # Convert results to list of dicts with parsed production orders
            data = []
            for row in results:
                production_order_list = [int(po) for po in row['production_orders'].split(',')] if row['production_orders'] else []
                data.append({
                    'ArticleNumber': row['ArticleNumber'],
                    'TotalMissing': row['total_missing'],
                    'TotalRequired': row['total_required'] or 0,
                    'ProductionOrders': production_order_list,
                    'ProductionOrdersCount': len(production_order_list),
                    'QuantityInProduction': row['quantity_in_production'] or 0,
                    'QuantityInProcurement': row['quantity_in_procurement'] or 0,
                    'id': row['ArticleNumber']  # For DataGrid compatibility
                })
            
            return data
    finally:
        conn.close()


@app.route('/warehouse/recalculate-missingcomponents', methods=['GET'])
def recalculate_missingcomponents():
    """
    Recalculates missing components from all non-completed production orders.
    Streams progress information as Server-Sent Events (SSE).
    Uses ThreadPoolExecutor for concurrent API requests to reduce total time.
    """
    headers = {"Authorization": "Bearer "+session['access_token']}
    
    def fetch_order_details(order_id):
        """Fetch a single order's details with retry logic."""
        backoff = 1.0
        for attempt in range(3):
            try:
                logging.info(f"Fetching details for order {order_id} (attempt {attempt + 1}/3)")
                response = requests.get(
                    url=FORTNOX_API_URI+f"api/warehouse/productionorders-v1/{int(order_id)}", 
                    headers=headers,
                    timeout=20  # Increased timeout for individual requests
                )
                if response.status_code == 429:
                    logging.warning(f"Fortnox rate limit hit (429) for order {order_id}; sleeping {backoff}s")
                    time.sleep(backoff)
                    backoff *= 2
                    continue
                response.raise_for_status()
                return response.json()
            except Exception as e:
                logging.warning(f"Attempt {attempt + 1}/3 failed for order {order_id}: {e}")
                if attempt == 2:
                    return None
                time.sleep(backoff)
                backoff *= 2
        return None

    def progress_generator():
        try:
            # Get all production orders
            try:
                response = requests.get(url=FORTNOX_API_URI+"api/warehouse/productionorders-v1?state=all", headers=headers, timeout=30)
                response.raise_for_status()
                all_orders = response.json()
            except Exception as e:
                logging.error(f"Failed to fetch production orders: {e}")
                yield f"data: {json.dumps({'error': str(e), 'done': True})}\n\n"
                return
            
            orders_list = all_orders.get('ProductionOrders', []) if isinstance(all_orders, dict) else all_orders
            if not orders_list:
                yield f"data: {json.dumps({'processed': 0, 'total': 0, 'skipped': 0, 'message': 'No production orders found', 'done': True})}\n\n"
                return
            
            # Filter for non-completed and non-voided orders
            non_completed_orders = [o for o in orders_list if o.get('documentState') not in ('completed', 'voided')]
            total_orders = len(non_completed_orders)
            processed = 0
            skipped = 0
            
            logging.info(f"Starting recalculation: {total_orders} non-completed orders to process")
            
            # Clear existing missing_articles records
            conn = None
            try:
                conn = get_db_connection()
                with conn.cursor() as cursor:
                    cursor.execute("DELETE FROM missing_articles")
                logging.info("Cleared missing_articles table")
            except Exception as e:
                logging.error(f"Failed to clear missing_articles: {e}")
            finally:
                if conn:
                    try:
                        conn.close()
                    except:
                        pass
            
            # Use ThreadPoolExecutor to fetch orders in parallel (max 3 concurrent requests)
            max_workers = 3
            results_dict = {}
            
            with ThreadPoolExecutor(max_workers=max_workers) as executor:
                # Submit all fetch tasks
                future_to_order = {}
                for order in non_completed_orders:
                    order_id = order.get('id') or order.get('Id')
                    if order_id:
                        future = executor.submit(fetch_order_details, order_id)
                        future_to_order[future] = order_id
                
                # Process results as they complete
                completed_count = 0
                for future in as_completed(future_to_order):
                    order_id = future_to_order[future]
                    completed_count += 1
                    
                    try:
                        response_data = future.result()
                        results_dict[order_id] = response_data
                    except Exception as e:
                        logging.error(f"Failed to fetch order {order_id}: {e}")
                        results_dict[order_id] = None
                    
                    # Send progress update every order fetched
                    progress_percent = (completed_count / total_orders * 100) if total_orders > 0 else 0
                    msg = f"data: {json.dumps({'processed': 0, 'total': total_orders, 'skipped': 0, 'progress': progress_percent, 'message': f'Fetching orders: {completed_count}/{total_orders}...', 'done': False})}\n\n"
                    yield msg
            
            # Now process all fetched data and store in database
            conn = None
            try:
                conn = get_db_connection()
                for idx, order in enumerate(non_completed_orders):
                    order_id = order.get('id') or order.get('Id')
                    
                    if not order_id:
                        skipped += 1
                        continue
                    
                    response_data = results_dict.get(order_id)
                    
                    if response_data is None:
                        logging.warning(f"No data for order {order_id}")
                        skipped += 1
                    else:
                        try:
                            with conn.cursor() as cursor:
                                inserted = 0
                                for row in response_data.get('packageItems', []):
                                    article_id = row.get('itemId') or row.get('id')
                                    quantity_missing = row.get('totalQuantityRequired', 0) - row.get('quantityReserved', 0)
                                    
                                    if quantity_missing > 0:
                                        total_required = row.get('totalQuantityRequired', 0)
                                        cursor.execute(
                                            "INSERT INTO missing_articles (ArticleNumber, ProductionOrder, quantity, quantity_required) VALUES (%s, %s, %s, %s) ON DUPLICATE KEY UPDATE quantity=VALUES(quantity), quantity_required=VALUES(quantity_required)",
                                            (article_id, int(order_id), quantity_missing, total_required)
                                        )
                                        inserted += 1
                                logging.info(f"Inserted {inserted} missing articles for order {order_id}")
                            processed += 1
                        except Exception as e:
                            logging.error(f"Failed to store data for order {order_id}: {e}")
                            skipped += 1
                    
                    # Send progress update after processing each order
                    progress_percent = ((idx + 1) / total_orders * 100) if total_orders > 0 else 0
                    msg = f"data: {json.dumps({'processed': processed, 'total': total_orders, 'skipped': skipped, 'progress': progress_percent, 'message': f'Processing: {idx + 1}/{total_orders} orders...', 'done': False})}\n\n"
                    yield msg
            finally:
                if conn:
                    try:
                        conn.close()
                    except:
                        pass
            
            # Final completion message
            logging.info(f"Recalculation complete: {processed} processed, {skipped} skipped out of {total_orders}")
            yield f"data: {json.dumps({'processed': processed, 'total': total_orders, 'skipped': skipped, 'progress': 100, 'message': f'Recalculation complete: {processed}/{total_orders} orders processed', 'done': True})}\n\n"
        except Exception as e:
            logging.error(f"Fatal error in recalculation generator: {e}", exc_info=True)
            yield f"data: {json.dumps({'error': f'Recalculation failed: {str(e)}', 'done': True})}\n\n"
    
    response = Response(progress_generator(), mimetype='text/event-stream')
    response.headers['Cache-Control'] = 'no-cache'
    response.headers['X-Accel-Buffering'] = 'no'
    return response


@app.route('/warehouse/orderplanning', methods=['GET'])
def warehouse_orderplanning():
    """
    Returns order planning data for all articles with missing components.
    """
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute("""
                SELECT 
                    op.id,
                    op.ArticleNumber,
                    op.amount,
                    op.price,
                    op.supplier_id,
                    op.supplier_currency,
                    op.supplier_order_number,
                    op.comment
                FROM orderplanning op
                ORDER BY op.updated_at DESC
            """)
            results = cursor.fetchall()
            return results if results else []
    except Exception as e:
        logging.error(f"Failed to fetch order planning data: {e}")
        return Response(json.dumps({'error': str(e)}), status=500, mimetype='application/json')
    finally:
        conn.close()


@app.route('/warehouse/orderplanning/<article>', methods=['GET'])
def get_orderplanning(article):
    """
    Returns order planning data for a specific article.
    """
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute("""
                SELECT 
                    id,
                    ArticleNumber,
                    amount,
                    price,
                    supplier_id,
                    supplier_currency,
                    supplier_order_number,
                    comment
                FROM orderplanning
                WHERE ArticleNumber = %s
            """, (article,))
            result = cursor.fetchone()
            return result if result else {'ArticleNumber': article, 'amount': 0, 'price': 0, 'supplier_id': None, 'supplier_currency': '', 'supplier_order_number': '', 'comment': ''}
    except Exception as e:
        logging.error(f"Failed to fetch order planning for {article}: {e}")
        return Response(json.dumps({'error': str(e)}), status=500, mimetype='application/json')
    finally:
        conn.close()


@app.route('/warehouse/orderplanning', methods=['POST'])
def save_orderplanning():
    """
    Save or update order planning data for an article.
    """
    try:
        data = json.loads(request.data)
        article = data.get('ArticleNumber')
        amount = data.get('amount', 0)
        price = data.get('price', 0)
        supplier_id = data.get('supplier_id')
        supplier_currency = data.get('supplier_currency', '')
        supplier_order_number = data.get('supplier_order_number', '')
        comment = data.get('comment', '')
        
        if not article:
            return Response(json.dumps({'error': 'ArticleNumber is required'}), status=400, mimetype='application/json')
        
        conn = get_db_connection()
        try:
            with conn.cursor() as cursor:
                cursor.execute("""
                    INSERT INTO orderplanning (ArticleNumber, amount, price, supplier_id, supplier_currency, supplier_order_number, comment)
                    VALUES (%s, %s, %s, %s, %s, %s, %s)
                    ON DUPLICATE KEY UPDATE
                    amount = VALUES(amount),
                    price = VALUES(price),
                    supplier_id = VALUES(supplier_id),
                    supplier_currency = VALUES(supplier_currency),
                    supplier_order_number = VALUES(supplier_order_number),
                    comment = VALUES(comment),
                    updated_at = CURRENT_TIMESTAMP
                """, (article, amount, price, supplier_id, supplier_currency, supplier_order_number, comment))
            
            # Return the saved record
            with conn.cursor() as cursor:
                cursor.execute("""
                    SELECT id, ArticleNumber, amount, price, supplier_id, supplier_currency, supplier_order_number, comment
                    FROM orderplanning
                    WHERE ArticleNumber = %s
                """, (article,))
                result = cursor.fetchone()
                return result
        finally:
            conn.close()
    except Exception as e:
        logging.error(f"Failed to save order planning: {e}")
        return Response(json.dumps({'error': str(e)}), status=500, mimetype='application/json')


@app.route('/warehouse/deliveries', methods=['GET'])
def get_deliveries():
    headers = {"Authorization": "Bearer "+session['access_token']}
    response = requests.get(url = FORTNOX_API_URI+"api/warehouse/deliveries-v1", headers=headers)
    response_data = response.json()
    return response_data

@app.route('/warehouse/deliveries/inbound/<id>', methods=['GET'])
def get_deliveries_inbound(id = 0):
    headers = {"Authorization": "Bearer "+session['access_token']}
    response = requests.get(url = FORTNOX_API_URI+"api/warehouse/deliveries-v1/inbounddeliveries/"+str(int(id)), headers=headers)
    response_data = response.json()
    return response_data

@app.route('/warehouse/deliveries/outbound/<id>', methods=['GET'])
def get_deliveries_outbound(id=0):
    headers = {"Authorization": "Bearer "+session['access_token']}
    response = requests.get(url = FORTNOX_API_URI+"api/warehouse/deliveries-v1/outbounddeliveries/"+str(int(id)), headers=headers)
    response_data = response.json()
    return response_data



def get_produtionorder_id_from_referrer():
    referrer_url = urlparse(request.referrer)
    url_parts = referrer_url.path.split('/')
    if len(url_parts) < 2:
        response = {
            'error'     : 'referrer error',
            'message'   : 'cannot parse referrer '+referrer_url,
        #    'params'    : params,
        #    'diff'      : differences,
        }
        return Response(json.dumps(response), status=500, mimetype='application/json')
    productionorder = int(url_parts[-1])
    if not productionorder != 0 or not url_parts[-2] == 'productionorder':
        response = {
            'error'     : 'wrong productionorder or type',
            'message'   : 'wrong productionorder id '+str(productionorder)+' or type '+url_parts[-2],
        #    'params'    : params,
        #    'diff'      : differences,
        }
        return Response(json.dumps(response), status=500, mimetype='application/json')
    return productionorder


@app.route('/save/stockchanges/', methods=['POST'])
def save_stockchanges():
    data = json.loads(request.data)
    date = data['date']

    productionorder = get_produtionorder_id_from_referrer()
    if not isinstance(productionorder, int):
        return productionorder
    



    if not 'newQuantityInStock' in session:
        return "no changes"
    if not productionorder in session['newQuantityInStock']:
        return "no changes"
        
    adjustment_down = {}
    adjustment_up = {}
    for key in session['newQuantityInStock'][productionorder]:
        diff = int(session['newQuantityInStock'][productionorder][key]['new']) - int(session['newQuantityInStock'][productionorder][key]['old'])
        if diff > 0:
            adjustment_up[key] = diff
        elif diff < 0:        
            adjustment_down[key] = diff


    headers = {"Authorization": "Bearer "+session['access_token'], 
               "Content-type" : "application/json"
    }
#https://api.fortnox.se/api/warehouse/deliveries-v1/inbounddeliveries/{id}
#    response = requests.get(url = FORTNOX_API_URI+"api/warehouse/deliveries-v1", headers=headers)
 #   response = requests.get(url = FORTNOX_API_URI+"api/warehouse/deliveries-v1/inbounddeliveries/12", headers=headers)
   # response = requests.get(url = FORTNOX_API_URI+"api/warehouse/deliveries-v1/outbounddeliveries/1", headers=headers)
   # response_data = response.json()
    data_in = {}
    response_data_in = {}
    url_in = ''
    if len(adjustment_up) > 0:
        data_in = {
            "date": date,
            "rows": [],
            "currency": "SEK",
            "currencyRate": 1,
            "currencyUnit": 1,
            "note": "Adjustment after productionorder "+ str(productionorder),
        }
        for key in adjustment_up:
            row = {
                "itemId":key,
                "quantity":adjustment_up[key],
            }
            data_in['rows'].append(row)
        url_in = FORTNOX_API_URI+"api/warehouse/deliveries-v1/inbounddeliveries"
        response_in = requests.post(url = FORTNOX_API_URI+"api/warehouse/deliveries-v1/inbounddeliveries", data = json.dumps(data_in), headers=headers)
        response_data_in = response_in.json()
        if "ErrorInformation" in response_data_in:
            response = {
                'error'     : response_data_in['ErrorInformation']['code'],
                'message'   : response_data_in['ErrorInformation']['message'],
                'params'    : data_in,
                'date':date,
                'up': adjustment_up,
                'down': adjustment_down,
                'documents': response_data_in,
                'in':data_in,
            }
            return Response(json.dumps(response), status=500, mimetype='application/json')


    data_out = {}
    url_out = ''
    response_data_out = {}
    if len(adjustment_down) > 0:
        data_out = {
            "date": date,
            "rows": [],
            "currency": "SEK",
            "currencyRate": 1,
            "currencyUnit": 1,
            "note": "Adjustment after productionorder "+ str(productionorder),
        }


        for key in adjustment_down:
            row = {
                "itemId":key,
                "quantity":-adjustment_down[key],
            }
            data_out['rows'].append(row)
        url_out = FORTNOX_API_URI+"api/warehouse/deliveries-v1/outbounddeliveries"
        response_out = requests.post(url = FORTNOX_API_URI+"api/warehouse/deliveries-v1/outbounddeliveries", data = json.dumps(data_out), headers=headers)
        response_data_out = response_out.json()
        if "ErrorInformation" in response_data_out:
            response = {
                'error'     : response_data_out['ErrorInformation']['code'],
                'message'   : response_data_out['ErrorInformation']['message'],
                'params'    : data_out,
                'date':date,
                'up': adjustment_up,
                'down': adjustment_down,
                'documents': response_data_out,
                'out':data_out,
                'in':data_in,
            }
            return Response(json.dumps(response), status=500, mimetype='application/json')




    return {'date':date,
            'up': adjustment_up,
            'down': adjustment_down,
            'response_in': response_data_in,
            'response_out': response_data_out,
            'out':data_out,
            'in':data_in,
            'url_in':url_in,
            'url_out':url_out,
            }


@app.route('/allarticles', methods=['GET'])
def update_allarticles():
    articles = {}
    i = 0
    for article in allarticles():
        i += 1
        if i<= 0:
            continue
        response = update_articles(article);
        response['id'] = i
        articles[article] = response
        time.sleep(0.45)
        if i >= 450:
            return articles

    return articles



def _generate_pdf_buffer(data, skip_cover):
    """Generate a PDF buffer from order data."""
    buffer = io.BytesIO()
    page_width, page_height = 2.75 * inch, 1.25 * inch
    
    pdf = canvas.Canvas(buffer, pagesize=(page_width, page_height))
    pdf.setFont("Helvetica", 10)
    if not (skip_cover and len(data) == 1):
        pdf.drawString(10, page_height - 20, f"{data[0]['documentid']}")
        pdf.drawString(10, page_height - 32, f"{data[0]['supplier']}")
        pdf.drawString(10, page_height - 44, f"{data[0]['orderdate']}")
        pdf.showPage()

    line = 0
    for row in data:
        line = line + 1
        pdf.setFont("Helvetica-Bold", 14)
        pdf.drawString(10, page_height - 20, f"{row['place']} {row['id']}")
        pdf.setFont("Helvetica", 9)
        pdf.drawString(10, page_height - 31, f"{row['Description']}")
        pdf.setFont("Helvetica", 12)
        pdf.drawString(10, page_height - 42, f"{row['QuantityInStock']}+{row.get('extra_amt',0)} pcs")
        pdf.drawString(10, page_height - 55, f"{row['comptype']}, V: {row['value']}, C: {row['case']}")
        pdf.drawString(10, page_height - 68, f"R: {row['rating']}, P: {row['precision']}")
        # orderedQuantity may not always be provided; omit if missing/empty
        ordered_qty = data[0].get('orderedQuantity')
        place_old = row.get('place_old')
        info_parts = [f"({data[0]['documentid']} line {line}"]
        if ordered_qty not in (None, ''):
            info_parts.append(f"{ordered_qty}pcs")
        if place_old not in (None, ''):
            info_parts.append(str(place_old))
        info = ", ".join(info_parts) + ")"
        pdf.drawString(10, page_height - 85, info)
        pdf.showPage()
        
    pdf.save()
    buffer.seek(0)
    return buffer


@app.route('/generate-pdf', methods=['POST'])
def generate_pdf():
    req = request.json
    data = req['data']
    skip_cover = req.get('skip_cover', False)
    buffer = _generate_pdf_buffer(data, skip_cover)
    return send_file(buffer, as_attachment=True, download_name=data[0]['documentid']+'.pdf', mimetype='application/pdf')


@app.route('/print-pdf', methods=['POST'])
def print_pdf():
    req = request.json
    data = req['data']
    skip_cover = req.get('skip_cover', False)
    buffer = _generate_pdf_buffer(data, skip_cover)

    # Save to temp file and print
    import tempfile
    with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp:
        tmp.write(buffer.getvalue())
        tmp_path = tmp.name

    try:
        print_pdf_to_zebra(tmp_path)
        return {'message': 'Printed successfully'}
    except Exception as e:
        return Response(json.dumps({'error': str(e)}), status=500, mimetype='application/json')
    finally:
        os.unlink(tmp_path)




@app.route('/warehouse/purchaseorders/list')
def warehouse_purchaseorders_list():
    headers = {"Authorization": "Bearer " + session['access_token']}
    response = requests.get(
        url=FORTNOX_API_URI + "api/warehouse/purchaseorders-v1?state=all",
        headers=headers, timeout=20
    )
    response_data = response.json()
    if isinstance(response_data, dict) and 'PurchaseOrders' in response_data:
        orders_list = response_data['PurchaseOrders']
    elif isinstance(response_data, list):
        orders_list = response_data
    else:
        orders_list = []
    active = [o for o in orders_list if o.get('purchaseOrderState') not in ('VOIDED', 'RECEIVED')]
    return json.dumps([{
        'id': o.get('id'),
        'supplierName': o.get('supplierName', ''),
        'supplierNumber': str(o.get('supplierNumber', '')),
        'orderDate': o.get('orderDate', ''),
        'purchaseOrderState': o.get('purchaseOrderState', '')
    } for o in active])


@app.route('/warehouse/create-purchaseorder', methods=['POST'])
def warehouse_create_purchaseorder():
    auth_headers = {"Authorization": "Bearer " + session['access_token']}
    headers = {**auth_headers, "Content-type": "application/json"}
    body = request.get_json()
    supplier_id = body.get('supplier_id', '')
    order_id = body.get('order_id')  # None = new order
    items = body.get('items', [])

    if not items:
        return Response(json.dumps({'error': 'no_items', 'message': 'No items provided'}), status=400, mimetype='application/json')

    rows = [{'itemId': it['ArticleNumber'], 'orderedQuantity': it['quantity'], 'price': it['price'], 'currencyCode': it.get('currency', '')} for it in items]

    if order_id is None:
        # Fetch supplier details for payment terms
        supplier_resp = requests.get(
            url=FORTNOX_API_URI + f"3/suppliers/{supplier_id}",
            headers=auth_headers, timeout=20
        )
        supplier_data = supplier_resp.json().get('Supplier', {})
        logging.info(f"Supplier data: {supplier_data}")
        payment_terms = supplier_data.get('TermsOfPayment', '')
        supplier_name = supplier_data.get('SupplierName') or supplier_data.get('Name', '')
        supplier_currency = supplier_data.get('Currency', 'SEK')

        # Fetch our company info for delivery address fields
        company_resp = requests.get(
            url=FORTNOX_API_URI + "3/companyinformation",
            headers=auth_headers, timeout=20
        )
        company_json = company_resp.json()
        logging.info(f"CompanyInformation response: {company_json}")
        company = company_json.get('CompanyInformation', {})
        delivery_name = company.get('CompanyName') or company.get('Name', '')
        delivery_address = company.get('Address', '')
        delivery_city = company.get('City', '')
        delivery_zip = company.get('ZipCode') or company.get('PostalCode', '')

        # If company info is missing, fall back to delivery fields from an existing PO
        if not all([delivery_name, delivery_city, delivery_zip]):
            list_resp = requests.get(
                url=FORTNOX_API_URI + "api/warehouse/purchaseorders-v1?state=all",
                headers=auth_headers, timeout=20
            )
            list_data = list_resp.json()
            existing_list = list_data if isinstance(list_data, list) else list_data.get('PurchaseOrders', [])
            active_ids = [o['id'] for o in existing_list if o.get('purchaseOrderState') not in ('VOIDED', 'RECEIVED')]
            for po_id in active_ids[:3]:
                detail_resp = requests.get(
                    url=FORTNOX_API_URI + f"api/warehouse/purchaseorders-v1/{int(po_id)}",
                    headers=auth_headers, timeout=20
                )
                detail = detail_resp.json()
                if detail.get('deliveryName') and detail.get('deliveryCity'):
                    delivery_name = delivery_name or detail.get('deliveryName', '')
                    delivery_address = delivery_address or detail.get('deliveryAddress', '')
                    delivery_city = delivery_city or detail.get('deliveryCity', '')
                    delivery_zip = delivery_zip or detail.get('deliveryZipCode', '')
                    break

        if not all([delivery_name, delivery_city, delivery_zip]):
            return Response(json.dumps({
                'error': 'missing_delivery_info',
                'message': 'Could not determine delivery address. Please ensure company information is filled in Fortnox, or create the first PO manually in Fortnox.',
                'company_info': company_json,
            }), status=500, mimetype='application/json')

        today = time.strftime('%Y-%m-%d')
        payload = {
            'supplierNumber': supplier_id,
            'supplierName': supplier_name,
            'orderDate': today,
            'currencyCode': supplier_currency,
            'paymentTermsCode': payment_terms,
            'deliveryName': delivery_name[:50],
            'deliveryAddress': delivery_address[:1024],
            'deliveryCity': delivery_city[:50],
            'deliveryZipCode': delivery_zip[:10],
            'rows': rows,
        }
        resp = requests.post(
            url=FORTNOX_API_URI + "api/warehouse/purchaseorders-v1",
            data=json.dumps(payload),
            headers=headers,
            timeout=30
        )
        result = resp.json()
        if 'ErrorInformation' in result:
            err = result['ErrorInformation']
            return Response(json.dumps({'error': err.get('code'), 'message': err.get('message')}), status=500, mimetype='application/json')
        # Check for validation errors (different error structure)
        if isinstance(result, dict) and result.get('error') == 'validation':
            return Response(json.dumps({'error': 'validation', 'message': str(result.get('fields', result))}), status=500, mimetype='application/json')
        return json.dumps(result)
    else:
        # Update existing purchase order — fetch current state first
        fetch_resp = requests.get(
            url=FORTNOX_API_URI + f"api/warehouse/purchaseorders-v1/{int(order_id)}",
            headers={"Authorization": "Bearer " + session['access_token']},
            timeout=20
        )
        existing = fetch_resp.json()
        if 'ErrorInformation' in existing:
            err = existing['ErrorInformation']
            return Response(json.dumps({'error': err.get('code'), 'message': err.get('message')}), status=500, mimetype='application/json')

        # Merge rows: update existing items, append new ones
        existing_rows = existing.get('rows', [])
        new_items_map = {it['ArticleNumber']: it for it in items}
        merged = []
        for row in existing_rows:
            item_id = row.get('itemId', '')
            if item_id in new_items_map:
                it = new_items_map.pop(item_id)
                row['orderedQuantity'] = it['quantity']
                row['price'] = it['price']
            merged.append(row)
        # Append any remaining new items not already in the PO
        for article_number, it in new_items_map.items():
            merged.append({'itemId': article_number, 'orderedQuantity': it['quantity'], 'price': it['price']})

        existing['rows'] = merged
        resp = requests.put(
            url=FORTNOX_API_URI + f"api/warehouse/purchaseorders-v1/{int(order_id)}",
            data=json.dumps(existing),
            headers=headers,
            timeout=30
        )
        result = resp.json()
        if 'ErrorInformation' in result:
            err = result['ErrorInformation']
            return Response(json.dumps({'error': err.get('code'), 'message': err.get('message')}), status=500, mimetype='application/json')
        return json.dumps(result)


def allarticles():
    return [
        "PIC18F2550-I/SO",
        "RADOX155-1.5mm2-G/Y",
        "RADOX125-2.5mm2-GREEN/YELLOW",
        "RLF35-8/12N",
        "RLF35-8/14N"
    ]
